<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nikolay Kazmin</title>
    <description>The latest articles on DEV Community by Nikolay Kazmin (@kazmin).</description>
    <link>https://dev.to/kazmin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3947563%2F60b0af8d-47eb-4c53-a772-feb96cdc9a7e.jpg</url>
      <title>DEV Community: Nikolay Kazmin</title>
      <link>https://dev.to/kazmin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kazmin"/>
    <language>en</language>
    <item>
      <title>Why I stopped chasing the Google Lighthouse performance score</title>
      <dc:creator>Nikolay Kazmin</dc:creator>
      <pubDate>Wed, 10 Jun 2026 07:37:33 +0000</pubDate>
      <link>https://dev.to/kazmin/why-i-stopped-chasing-the-google-lighthouse-performance-score-415d</link>
      <guid>https://dev.to/kazmin/why-i-stopped-chasing-the-google-lighthouse-performance-score-415d</guid>
      <description>&lt;p&gt;I co-founded Domestina in 2014 as a Rails monolith, and for the past 7 years I've been its only engineer. SEO is our main growth channel, so a slow mobile landing page and a Lighthouse score of 44 had me genuinely worried.&lt;/p&gt;

&lt;p&gt;Last week I sat down to fix it properly. &lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest win was unglamorous
&lt;/h2&gt;

&lt;p&gt;One ~360KB CSS bundle was render-blocking every page. It included all of Bootstrap, the full icon set, and two datepickers (react-datepicker, flatpickr) that our landing pages never even load. A landing page was downloading the whole app's CSS just to paint an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That monolith used to be a best practice, the app was born on Sprockets, where concatenating everything into one fingerprinted, far-future-cached application.css was the documented best practice. On HTTP/1.1, fewer requests beat smaller payloads. HTTP/2 multiplexing and Core Web Vitals quietly inverted that tradeoff. The problem was also that for 12 years, were were just piling styles into one big file, unrealizing how big it had become.&lt;/p&gt;

&lt;h2&gt;
  
  
  The optimizations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I restructured the CSS into &lt;strong&gt;page-set bundles&lt;/strong&gt; on the existing cssbundling-rails + dart-sass setup: shared.bundle (Bootstrap subset + global chrome), then public.bundle, funnel.bundle, dashboard.bundle, blog.bundle. A landing page now loads shared + public only.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;icon CSS rules&lt;/strong&gt; were subsetted, but the actual .woff2 was still the full ~2000-glyph file: 131KB → 10.5KB once properly subset (via subset-font, harfbuzz-wasm, no Python). One gotcha: bin/build_icons.js scans for literal bi-* tokens, so dynamically built names like &lt;code&gt;"bi-#{x}"&lt;/code&gt; aren't detected and silently drop from the subset. It also has to scan the Ruby that emits icon markup, not just the views.&lt;/li&gt;
&lt;li&gt;Domestina operates in 6 countries, each country running its own instance of the web app, and each comes with its own set of locales.  All &lt;strong&gt;9 i18n catalogs, around 754KB, were bundled into every market&lt;/strong&gt; because &lt;code&gt;require()&lt;/code&gt; inside an object literal is eager. The runtime filter only chose which catalog to store, not which one to bundle. I genuinely thought I'd fixed this before. The real fix was generating a per-market active_locales.generated.js from the market's supported_locales. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This moved the score from &lt;strong&gt;44 to the low 70s&lt;/strong&gt; in one shot. FCP and Speed Index both went from 5.8s to 1.7s.&lt;/p&gt;

&lt;h2&gt;
  
  
  I pressed further
&lt;/h2&gt;

&lt;p&gt;I deferred a render-blocking script — a no-brainer win — and the score went down. The culprit was LCP at ~7s. But the observed LCP was ~440ms in every run. The page painted fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7s number wasn't real
&lt;/h2&gt;

&lt;p&gt;The 7s was Lighthouse's Lantern simulation. Lighthouse runs the page quickly, then estimates timings on a throttled profile: Moto G Power, 1.6 Mbps, 150ms RTT, 4× CPU slowdown.&lt;/p&gt;

&lt;p&gt;Under that model, simulated LCP collapses toward total bytes divided by bandwidth. The tell was that Lighthouse's own metricSavings for LCP was 0ms on every audit. There was no single fixable resource left.&lt;br&gt;
And the lab score is not what Google ranks on anyway. For that, Google uses CrUX field data: real Chrome users, 28-day window, 75th percentile. Ours was 100% of mobile URLs "Good", zero over 2.5s LCP, stable for weeks.&lt;br&gt;
The page was already fast for real people.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I called it a day
&lt;/h2&gt;

&lt;p&gt;Hitting a green 90 would mean optimizing for a low-end phone on a connection slower than almost anyone in our market uses. The byte cuts still help genuine low-end users, so they weren't wasted. But past that point I'd be polishing an artifact, not improving the business.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In Lighthouse simulate mode, a bad LCP can be a function of total page weight divided by throttled bandwidth, not paint timing. Look at observed LCP  and &lt;code&gt;metricSavings&lt;/code&gt;before believing it.&lt;/li&gt;
&lt;li&gt;Judge performance by field data (CrUX / Search Console), not a single-run lab score. &lt;strong&gt;That's also what Google ranks on.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; On a Rails/Bootstrap monolith, the highest-leverage fix is usually unglamorous: get render-blocking CSS down to critical-only.&lt;/li&gt;
&lt;li&gt;"I already fixed that" deserves a second look. Filtering usage is not the same as reducing bundle bytes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop when the fixes stop helping real users.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest footnote on the year: I'd tried a version of this last summer and got nowhere. What got me through this time was the latest Claude Code,  that could actually follow the thread and push back when I was confidently wrong. I gave it plenty to work with.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>lighthouse</category>
      <category>rails</category>
      <category>seo</category>
    </item>
    <item>
      <title>Migrating Bootstrap 3 to 5 with Claude Code: every gotcha that isn't in the docs</title>
      <dc:creator>Nikolay Kazmin</dc:creator>
      <pubDate>Sat, 23 May 2026 12:35:14 +0000</pubDate>
      <link>https://dev.to/kazmin/migrating-bootstrap-3-to-5-with-claude-code-every-gotcha-that-isnt-in-the-docs-49d7</link>
      <guid>https://dev.to/kazmin/migrating-bootstrap-3-to-5-with-claude-code-every-gotcha-that-isnt-in-the-docs-49d7</guid>
      <description>&lt;p&gt;I started &lt;a href="https://www.domestina.bg/en" rel="noopener noreferrer"&gt;Domestina &lt;/a&gt;in 2014, and like many apps from that period, the first version was built on Bootstrap 3.1.&lt;/p&gt;

&lt;p&gt;At some point we updated to 3.3, but after that the frontend stack mostly froze, because the migration never made sense commercially. It always looked like 1–2 weeks of focused work, a lot of regression risk, and no visible customer value — while the backlog was full of things that customers actually asked for.&lt;/p&gt;

&lt;p&gt;Then came Opus 4.6 and over the last few months I managed to ship a large part of the feature backlog that had been sitting around for months and years. That finally gave me room to attack some old technical debt, and Bootstrap, as by far the oldest lib that I used, was a natural place to start.&lt;/p&gt;

&lt;p&gt;The migration was done in two very different phase.The first part was surprisingly fast. I gave my custom orchestrator a structured task brief, let it run, and came back about 4 hours later to a mostly working Bootstrap 5 bundle. It handled the mechanical work pretty well: dependency changes, the new SCSS pipeline, class renames, react-bootstrap 2.x, glyphicon → bootstrap-icons, and so on.&lt;/p&gt;

&lt;p&gt;The second part was slower and much more manual: about 2 days of going page by page with Claude, fixing the things that only show up when a real, old production app starts running on Bootstrap 5.&lt;/p&gt;

&lt;p&gt;I think that with the new AI tools, we'll have a lot of people starting to pay off technical debt. That's why I decided to summarize and share my experience with this migration with the hope that it will save some time to a lot of fellow devs.&lt;/p&gt;

&lt;p&gt;Paste it into Claude, ChatGPT, Cursor, etc. before starting a Bootstrap 3 → 5 migration and it should save you from a lot of the weird bugs that are not obvious from the official docs.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/kazmin/bootstrap-3-to-5-migration-prompt" rel="noopener noreferrer"&gt;https://github.com/kazmin/bootstrap-3-to-5-migration-prompt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Framework-agnostic. MIT licensed. PRs welcome.&lt;/p&gt;

</description>
      <category>css</category>
      <category>bootstrap</category>
      <category>claude</category>
      <category>promptengineering</category>
    </item>
  </channel>
</rss>
