Notebook · Note 001 · Web performance
Where a static site hid the last 18 points
Images were compressed, JavaScript was three files, and there was no framework. Lighthouse still gave 82. The remaining points were in three places, all named by the tool but not explained.
Founder, BLN Global · · 7 min read
Short answer
With the images and JavaScript already in good shape, the remaining points came down to three issues. First, the check meant to skip unnecessary work was reading the layout and forcing the browser to recalculate it. Second, the stylesheet and external fonts delayed the first paint by 600 ms. We inlined the CSS and moved the fonts to our server. Third, 6 of the accessibility audit’s 24 contrast warnings were false positives. A full crawl then uncovered 13 pages that sampling had missed.
This site has no framework. Pages are plain HTML generated by Python scripts. Styles use one CSS file. JavaScript uses three small files. Images are generated as WebP in three sizes. So all the obvious performance work was already done.
The mobile score was stuck at 82.
When the obvious stuff was done, the remaining points were not in one big mistake. They sat in three separate places, one piece each. Finding all three was less about reading the tool's warning and more about the warning's what you mean required understanding.
First place: the check written to skip the work
Lighthouse's "forced reflow" warning says JavaScript made the browser calculate page layout too early. The browser batches style changes and applies them in one pass, but if a measurement read slips in, it has to flush the batch at once.
The warning pointed to the source line, but the line had no visible measurement. It took three rounds.
- In the first round
getComputedStylecalls were removed. The score did not change. - On the second pass, the real culprit showed up: a check written
if (window.scrollY > 0)to skip unnecessary work. The check itself is a layout read. The line written to skip work made you do the work you wanted to skip. - On the third pass, the function was deleted. The initial state was already in CSS. JavaScript did not need to rebuild it.
One remaining pattern also void element.offsetWidth was. A common, accepted trick that forces layout on purpose to restart a transition. In its place, two consecutive requestAnimationFrame was added. It does the same job without forcing the layout.
function restart(fn) { /* If the tab is in the background, rAF never runs and the demo stays blank. */ if (document.hidden) { fn(); return; } requestAnimationFrame(function () { requestAnimationFrame(fn); }); }
The lesson was this: a performance warning often does not say "you do too much work", it says "you do the work at the wrong time".
Second place: the 600 ms before first paint
The second warning covered render-blocking resources. When the page opened, the browser drew nothing until it downloaded and processed the stylesheet, then the fonts. The measured delay was 600 milliseconds.
We inlined the style sheet
A separate CSS file comes from cache on the second visit. That helps. But it adds a round trip on the first visit, which is what visitors from search experience. We minify CSS at publish time and embed it in the page. We measured it: after Brotli compression, the embedded version was smaller than a separate file + second request.
We moved the fonts to our own server
The setup from Google Fonts connected to two separate origins: one for style fonts.googleapis.com, for files fonts.gstatic.com. Each new origin needs DNS resolution, a TLS handshake and a new connection. Removing that produced this measured change:
Font downloaded by a Turkish page. 3 files from one origin instead of 6 files and 2 external origins.
unicode-range fields to isolate the subsets a Turkish page would actually download (latin + latin-ext), then measured every file. The counterpart in this repository assets/fonts real size of its files.How the fonts shrank and where the cost landed while opening to ten languages became a separate note: ten languages, three font files.
Third place: where the tool gets it wrong
The accessibility audit gave 24 color contrast warnings. If we had accepted and fixed them, half the design would have broken. 6 warnings were not real.
Reason content-visibility:auto was enabled. This feature speeds up the first paint by delaying off-screen rendering. Exactly what we wanted. But the deferred section’s computed color never reached the audit tool, so it reported white text on a white background. Nothing like that appeared on screen.
Real ratios were measured one by one in the browser. 18 warnings were real and fixed. 6 were blind spots in the tool, content-visibility removed only from the two problem sections and kept elsewhere.
An audit tool warning is not proof of a bug to fix. A warning is a claim. You check it, then fix it. This lesson came up two more times on this site. One was in the language review note is described.
Then there is the wound we caused
To prevent layout shifts, all images get width and height attribute was added. Good move. The browser can reserve space before downloading the image. But the demo boxes broke. Some images rendered 58 pixels wide and 1500 pixels tall.
Reason is this: in HTML, width and height attributes are presentational hints, while those in CSS height fill in the value. In the design, those boxes only have aspect-ratio was defined, height was empty. The attribute landed there and crushed the ratio. One-line fix:
img{max-width:100%;height:auto;display:block}
And where the crawl actually pays off
When the home page hit 100, it looked done. It wasn't. What we measured was a sample. The site's all 150 pages pages were run through Lighthouse one by one. Result: 137 pages scored 100 in all four categories, not 13 pages. Accessibility, Best Practices and SEO were 100 on every page. Only performance moved.
Eleven of the thirteen pages were in the same language: Arabic. The lowest score was 83.
/ar/safecoast/ 83 /ar/laya/ 84 /ar/localmind/ 84 /ar/adforge/ 85 /ar/garanti/ 85 /ar/gtin/ 86 /ar/visionguard/ 86 /ar/ 89 /ar/growthos/ 97 /ar/dream/ 99 /ar/twin/ 99
Lighthouse gave the exact reason: Web font loaded → ar.woff2. The Arabic font arrived late and laid out the page again. Its fallback had very different metrics from Noto Sans Arabic, so the shift was substantial. The fix was the same choice we had already made for Martian Mono: swap not optional.
Layout shift (CLS) on Arabic pages
The main lesson here isn’t about performance. If you sample a few pages and say the site scores 100, you’re making claims about pages you didn’t measure. Fifteen pages sat at 83 for months because we never checked them.
The result’s limit
The score is a lab measurement. It assumes a fixed device and network model. A real visitor’s phone and connection may perform worse. Run the same page several times and the score moves between 98 and 100. A score of 100 does not mean the page is fast for everyone. It means the measurable blockers have been removed. Once enough real-user data comes in, Chrome’s field data will be the metric to watch.
These three fixes share one thing. None involved doing more. Each removed work done at the wrong time or work that should never happen.