
Published September 8, 2026 · 7 min read
This portfolio combines animated diagrams, project videos, articles, and an AI assistant. Each feature has a purpose, but visitors should not have to pay its full loading cost before they use it.
On 6 September 2026, I audited the application and worked through six performance changes with AI-assisted implementation and verification. Home's initial JavaScript fell from 325,647 to 262,673 gzip bytes, a 19.3% reduction in the compared local production builds. Contact's initial JavaScript fell by 20.9%.
Those numbers describe a specific build comparison. They are not a Lighthouse score, a live Core Web Vitals result, or evidence of improved hiring or customer outcomes. This case study explains the changes, how they were measured, and what remains unresolved.
The baseline was the repository at commit 591fa53. The completed performance work was committed as 37cf978. For each route, the audit summed the gzip-compressed size of unique JavaScript files referenced by the generated initial HTML, excluding the nomodule polyfill.
This gives a repeatable measure of initial script delivery. It excludes route prefetches and features loaded later. The live deployment may use Brotli, so these totals must not be reported as the exact bytes a visitor downloads.
| Route | Before, gzip bytes | After, gzip bytes | Reduction |
|---|---|---|---|
| Home | 325,647 | 262,673 | 19.3% |
| About | 294,160 | 259,839 | 11.7% |
| Work | 294,764 | 260,364 | 11.7% |
| Blog | 281,228 | 239,860 | 14.7% |
| Contact | 386,294 | 305,621 | 20.9% |
| TSConfig article | 286,558 | 245,697 | 14.3% |
These results belong to the performance revision, before the later employer-content rewrite. New content and dependencies can change a future build; the table is a dated record, not a permanent performance guarantee.
Home imported an About feature and preloaded the About portrait even though it did not display that portrait. Removing the dependency and preload reduced Home's initial gzip JavaScript by about 23.1 KB at that step. Browser checks confirmed that Home stopped requesting the portrait while About retained its priority image.
The shared layout also needed a narrower boundary. Deferring the chat container and separating feature styles reduced shared delivery costs. A navigation boundary prevented Contact from requesting Home's page chunk. Tailwind scanning was limited to application source so unrelated files did not influence generated styles.
This follows the Next.js guidance on Server and Client Component boundaries: keep browser behavior in focused client entries and inspect what those entries import. A component's visual size does not tell you the size of its dependency graph.
The practical lesson was to inspect route assets before trying to optimize individual render functions. An unnecessary feature dependency can cost more than a small component's repeated rendering.
The assistant has three distinct moments: the closed launcher, the welcome screen, and the conversation with Markdown rendering. They do not all need the same code at the same time.
The revised application defers the container until opening and loads the conversation separately from the welcome screen. In one paired cold mobile check, scripts fetched after opening fell from 296,766 to 140,233 bytes. Welcome readiness changed from 3.82 to 3.21 seconds. Those byte totals included concurrent navigation prefetches, and entering Messages later fetched another 160,314 bytes.
This was a shift in when work happens, as well as a reduction in the welcome path. The conversation still has a cost when it is used. That distinction matters: moving code behind an interaction does not make the code disappear.
The change also exposed a focus-restoration race. A dialog could restore focus after the composer or restored conversation had already received it. The fix preserved the intended initial focus without overriding a visitor who had moved to another control. Behavior tests covered persistence, errors, and Markdown safety alongside the loading changes.
The Contact background map was a decorative 1584 × 800 PNG. Re-encoding it with a 16-color palette preserved its dimensions and reduced the file from 66,652 to 15,120 bytes, about 77%. Its mean alpha difference was 0.373 on a 0–255 scale, and I inspected the rendered result.
Project posters had a different problem: their responsive size declarations did not match the capped layout. At 1440 pixels wide and DPR1, the posters rendered around 590 pixels wide but selected 1080-pixel resources. Correcting the size declarations selected 640-pixel resources instead. The combined poster payload fell from 57,690 to 27,594 bytes, about 52%.
The poster change improves delivery while scrolling. The posters are below the initial viewport, so it is not an initial LCP improvement claim. Browser checks covered several viewport and pixel-density combinations, including 390 pixels at DPR1 and DPR2.
The canvas previously repeated geometry work and scheduled drawing more often than the visual needed. The implementation now caches dot geometry, batches drawing into 32 brightness groups, and caps scheduling at 24 frames per second. Canvas and decorative animations pause outside the viewport or when the tab is hidden. Reduced motion keeps a static visual.
In local desktop samples, the CTA's main-thread activity fell from approximately 45% to 34.0–35.6%. Script time over four seconds fell from roughly 922 milliseconds to 417–471 milliseconds. These samples varied with host load.
The visible hero still recorded substantial work, including a 50.6% sample. That is a remaining performance concern. The measurement is renderer task time divided by elapsed time, not total machine CPU or GPU utilization.
The original performance revision passed TypeScript, ESLint, its 493 tests, and the production webpack build. Ego Lite browser checks covered navigation, responsive images, video visibility, reduced motion, chat transitions, focus, and article rendering. No real contact submissions or chat requests were sent.
For mobile timing, the audit used a 390 × 844 viewport, DPR2, 4× CPU slowdown, 150 ms latency, 200,000 bytes per second download, and a disabled cache. Contact LCP used three runs per version: its local median changed from 2.912 to 2.536 seconds. Chat used only one paired timing run, so that result has less confidence.
A follow-up after deployment should compare live behavior and field data under the same definitions. It should also revisit the visible hero's animation cost. Local measurements are useful evidence for a change; they cannot substitute for what real visitors experience.
The largest gains came from choosing when and where work belongs: remove an unrelated route dependency, delay conversation rendering until it is needed, request images at their actual display size, and pause invisible animation.
For the next performance investigation, I would start with the same questions: what does this route load, what does the visitor need now, and which measurement will show whether the change helped? Then I would check that keyboard behavior, reduced motion, and feature loading still work.
591fa53 with the performance revision committed as 37cf978. The relevant method and results are reproduced above.Occasional articles on React, full-stack development, performance, and AI workflows.