← All writeups

Client work, performance and security

Speeding up two client sites

PageSpeed scores of 70 and 69 on phones. The causes: a page that stayed invisible until JavaScript loaded, a CSS compiler running in the browser, and a CSP that silently blocked the analytics and the ads.

Client work. Two production sites I built and maintain: Формат7, an online print shop (Next.js and Python), and Печати7, a stamp and seal order site (Flask). October 2026.

Why I looked

Both sites worked, took orders and never threw errors at customers. Then I ran them through Google PageSpeed Insights on a phone profile and got 70 and 69 for performance, with the main content of the page appearing after 7.6 and 6.1 seconds. For a shop that people open from a link in a messenger, that is the first impression.

PageSpeed Insights for Формат7 before the fixes, mobile: performance 70, Largest Contentful Paint 7.6 s

I went through the reports item by item, reproduced each problem locally with Lighthouse, and found that most of the slowness came from a few decisions that looked harmless when they were made. Two of the findings were not about speed at all: on both sites the security policy I had written was silently breaking a third-party script.

Формат7: the whole site was invisible until JavaScript loaded

The Lighthouse trace said the largest element on the page had loaded in a fraction of a second but was painted about 5 seconds later. The image was there; the browser just was not allowed to show it.

The cause was a page transition wrapper in the root layout:

<motion.div
  initial={{ opacity: 0, y: 10 }}
  animate={{ opacity: 1, y: 0 }}
>
  {children}
</motion.div>

Next.js renders this on the server with opacity: 0, so every page of the site arrived in the browser fully transparent and stayed that way until React had downloaded, hydrated and started the animation. On a slow phone connection that is seconds of a white screen. Worse, the animation never played where it was meant to: the root layout is not remounted when you move between pages, so the wrapper only ever ran once, on the first load, exactly where it hurt. I removed it, along with the entrance animation on the first screen.

Формат7: photos stored as PNG

Six product photos were PNG files of 0.3 to 1 MB each, 4.4 MB together. They are photographs with no transparency, so PNG was simply the wrong format. The catch was that their paths, like /products/photo.png, are stored in the database and used by the admin panel, so renaming the files would mean a data migration on a live shop.

Instead I converted them to WebP (343 KB for all six) and added a rewrite in the Next.js config, so a request for the old .png address is answered with the WebP file. Nothing in the database changed. I also recompressed the JPEG photos and stopped the phone version from downloading two large images that are only shown on desktop, by wrapping them in a <picture> element with a desktop-only source.

Формат7: the analytics counted nothing

While reading the browser console I noticed the Content Security Policy blocking mc.yandex.ru. Yandex Metrica, the analytics the shop relies on, was being loaded on every page and immediately refused by the site's own CSP. The site owner was looking at visitor statistics that the site had never been able to send.

The fix was one line in the policy, allowing the Metrica domains for scripts, images, connections and frames. It has a visible cost: Lighthouse's best practices score dropped from 92 to 75, because Metrica sets third-party cookies. That is a trade-off the business wants, so I kept it and wrote down why.

Печати7: a CSS compiler running in every visitor's browser

Печати7 loaded Tailwind CSS from cdn.tailwindcss.com. That is the Play CDN, a roughly 400 KB script that scans the page and generates the CSS inside the visitor's browser. Tailwind's own documentation says it is for prototyping, not production. It blocks rendering, and it was the biggest item in the render-blocking requests that PageSpeed estimated at 3.3 seconds of savings.

I compiled the styles ahead of time with the Tailwind CLI, using the same configuration that was inlined in the page. The result is a 29 KB CSS file served by the site itself. The Inter font and the Font Awesome icons were also loaded from Google and cdnjs; I moved them onto the site, made the icon stylesheet load without blocking the first paint, and removed four external origins from the CSP. The hero illustration, a 373 KB PNG shown at 320 pixels, became a 61 KB WebP.

Печати7: an ad block that never showed an ad

The template contained a Yandex Advertising Network block. Its loader script was not allowed by the CSP, so the browser refused it on every page view, and the only trace was an error in the console. The block had never displayed anything. I removed it; if ads are wanted, it is a one-line policy change to turn them on properly.

PageSpeed Insights for Печати7 before the fixes, mobile: performance 69, First Contentful Paint 3.9 s

Accessibility

Both sites lost points for things that real users notice:

  • Contrast. White text on the brand orange #F97316 has a contrast ratio of 2.8:1; the minimum for normal text is 4.5:1. I used a deeper orange, #C2410C (5.2:1), for the two places with white text on orange, and darkened the light grey headline words and small captions.
  • Buttons without names. The mobile menu button and the icon-only login link had no text a screen reader could announce.
  • Heading order. Footer and feature headings skipped levels, so I fixed the tags without changing how they look.
  • A false alarm worth fixing anyway. On Печати7, Lighthouse reported a white heading on a near-white background. In reality the heading sits on blue, but two blurred decorative circles were separate elements behind it and confused the contrast check. I redrew them as CSS gradients, which look the same and leave nothing for the checker to misread.

Results

Measured with Lighthouse 12 on the mobile profile, before (PageSpeed Insights on the live sites) and after (local run of the fixed code with the same settings):

Performance Accessibility Best practices SEO
Формат7, before 70 88 92 100
Формат7, after 92 100 75 100
Печати7, before 69 80 92 100
Печати7, after 82 100 100 100

On desktop both sites now score 99 for performance. The local Печати7 number is held back by my test server, which does not compress responses; in production the site sits behind Caddy with gzip and zstd.

What I took from it

  • A security header can quietly break a feature. On both sites the CSP did its job and blocked a script nobody had allowed, and nobody noticed because nothing visibly failed. Now I check the console after every policy change, not only the page.
  • Read the trace, not just the score. "Largest Contentful Paint 7.6 s" sounds like a slow image. The trace showed the image arriving almost at once and then waiting for permission to appear.
  • Fix it where it can be fixed safely. Renaming files would have been the clean solution for the PNG photos, but on a live shop with paths in the database a rewrite rule got the same result with no migration.

Contact

Email is the fastest way to reach me. I usually reply within a day.

Download CV (PDF)

Elsewhere

LinkedInTryHackMeYouTubeAstroBinDeepSkyHosting35AwardsTelegram (astrophoto)Instagram (astrophoto)

The PRISM source code is available as a download on the Projects page. My GitHub account is currently under review.