<?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>Forem: Ashish Kumar</title>
    <description>The latest articles on Forem by Ashish Kumar (@helloashish99).</description>
    <link>https://forem.com/helloashish99</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%2F3861766%2F0b3f531d-15d0-4bfa-975a-ce54df37aac8.png</url>
      <title>Forem: Ashish Kumar</title>
      <link>https://forem.com/helloashish99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/helloashish99"/>
    <language>en</language>
    <item>
      <title>Meta StyleX: Moving CSS-in-JS From Runtime to Build Time</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 09:57:48 +0000</pubDate>
      <link>https://forem.com/helloashish99/meta-stylex-moving-css-in-js-from-runtime-to-build-time-242i</link>
      <guid>https://forem.com/helloashish99/meta-stylex-moving-css-in-js-from-runtime-to-build-time-242i</guid>
      <description>&lt;p&gt;Meta ships &lt;strong&gt;StyleX&lt;/strong&gt; as the styling layer behind very large surfaces, not because CSS is broken, but because classic &lt;strong&gt;runtime CSS-in-JS&lt;/strong&gt; struggles to scale when huge component trees and variant matrices meet strict performance budgets.&lt;/p&gt;

&lt;p&gt;The bet is simple to state and hard to execute: move styling work off the main thread’s long tail and into the &lt;strong&gt;bundler&lt;/strong&gt;, so the browser mostly loads deterministic &lt;strong&gt;static CSS&lt;/strong&gt; and applies &lt;strong&gt;atomic class names&lt;/strong&gt; instead of synthesizing rules on every navigation and interaction.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58wcgzw6d6g205f812zh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58wcgzw6d6g205f812zh.png" alt="Flow diagram of StyleX compile-time styling: JS style definitions are compiled into atomic static CSS for the browser." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What goes wrong with runtime CSS-in-JS
&lt;/h2&gt;

&lt;p&gt;Many CSS-in-JS libraries ask the runtime to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Execute style functions or objects when components render.&lt;/li&gt;
&lt;li&gt;Serialize declarations into strings or hashes.&lt;/li&gt;
&lt;li&gt;Inject new rules into &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags or a stylesheet registry during hydration and updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That model is ergonomic for product teams: colocation, themes, dynamic props. It also carries costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript time on meaningful render paths for style resolution.&lt;/li&gt;
&lt;li&gt;Larger bundles for runtime helpers.&lt;/li&gt;
&lt;li&gt;Harder caching and deduplication when rules appear late or per app.&lt;/li&gt;
&lt;li&gt;Inconsistent injection strategies across repos and micro-frontends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At Meta-scale codebases, those margins compound.&lt;/p&gt;




&lt;h2&gt;
  
  
  How StyleX reframes the problem
&lt;/h2&gt;

&lt;p&gt;StyleX keeps the &lt;strong&gt;authoring surface in JavaScript&lt;/strong&gt; (objects analyzed by a compiler) but does not treat the browser as the first place styles materialize. A compiler plugin runs at &lt;strong&gt;build time&lt;/strong&gt; and:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Statically reads style definitions tied to components.&lt;/li&gt;
&lt;li&gt;Emits &lt;strong&gt;atomic&lt;/strong&gt; classessmall, reusable rules so duplicates collapse globally.&lt;/li&gt;
&lt;li&gt;Writes plain CSS files or chunks the document loads like any other static asset.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Atomic CSS is the mechanical trick that makes dedup cheap: many call sites reuse the same small declaration set instead of generating nearly identical rules at runtime.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://stylexjs.com/" rel="noopener noreferrer"&gt;StyleX documentation&lt;/a&gt; is the source of truth for API details and constraints; this post stays at architecture level.&lt;/p&gt;




&lt;h2&gt;
  
  
  Runtime vs build time (one table)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Classic runtime CSS-in-JS&lt;/th&gt;
&lt;th&gt;StyleX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JS objects / tagged templates&lt;/td&gt;
&lt;td&gt;JS objects (build-time analyzable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When CSS exists&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;During render / effects&lt;/td&gt;
&lt;td&gt;After compile, before deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delivery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Injected rules + runtime&lt;/td&gt;
&lt;td&gt;Static CSS files + class names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dedup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per session / heuristic&lt;/td&gt;
&lt;td&gt;Global, at bundle time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JS + injection + recalculation&lt;/td&gt;
&lt;td&gt;Mostly parse + apply in the CSS engine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; StyleX still ships a small runtime for conditional composition and debuggingthe win is vastly smaller than fully dynamic injection pipelines, not literally zero bytes of JS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why “compile time” is the headline
&lt;/h2&gt;

&lt;p&gt;Frontend performance arguments often start with bundle size and end with main-thread contention. Moving styling to build time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shrinks what must execute after load for style synthesis.&lt;/li&gt;
&lt;li&gt;Turns CSS into cacheable assets (CDN, HTTP cache, preload).&lt;/li&gt;
&lt;li&gt;Aligns design systems with static analysis: lint rules, dead-code elimination, predictable class inventory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is the one you sketched: push work left on the timelinefrom runtime discovery to build extractionso production pays less per paint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;StyleX is Meta’s bet that large teams still want CSS-in-JS ergonomics without paying continuous runtime tax. Compilers extract styles, atomic classes deduplicate output, and static CSS restores predictable behavior for enormous trees. Not every app needs that machinery; when injection cost shows up in profilers, compile-time styling is the lever they chose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/meta-stylex-compile-time-styling/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 4 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stylex</category>
      <category>css</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>DRM PiP Loophole: Why Picture-in-Picture Can Bypass DRM Black-Screen Protection</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 08:57:48 +0000</pubDate>
      <link>https://forem.com/helloashish99/drm-pip-loophole-why-picture-in-picture-can-bypass-drm-black-screen-protection-1f83</link>
      <guid>https://forem.com/helloashish99/drm-pip-loophole-why-picture-in-picture-can-bypass-drm-black-screen-protection-1f83</guid>
      <description>&lt;p&gt;In the last post on &lt;a href="https://renderlog.in/blog/drm-screen-capture-jiohotstar/" rel="noopener noreferrer"&gt;why JioHotstar goes black when you screen share&lt;/a&gt;, DRM keeps decrypted video off the capture surface most of the time. In practice, people sometimes notice a gap: after popping the same stream into Picture-in-Picture (PiP), a screen recorder may suddenly see pixels again.&lt;/p&gt;

&lt;p&gt;This article explains why that can happen in the browser at a systems level, how Widevine L3 vs L1 fits in, and why no vendor promises this behavior forever. It is an implementation and policy balance, not a proof that protection is “unbreakable.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; Streaming terms of service and copyright law still apply. The goal is to understand compositor and CDM behavior, not to advise circumventing rights management for redistribution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs96qr1ftovw5ukxpljw2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs96qr1ftovw5ukxpljw2.png" alt="Diagram comparing in-page DRM video compositing versus Picture-in-Picture surfaces and why capture behavior can differ." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What people observe (browser + PiP)
&lt;/h2&gt;

&lt;p&gt;Reported steps: results vary by OS, GPU, browser build, codec path, and title tier:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a DRM-protected site such as JioHotstar in a desktop browser.&lt;/li&gt;
&lt;li&gt;Start playback, then enter PiP via a built-in browser control or an extension that wraps &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; in a floating window.&lt;/li&gt;
&lt;li&gt;Start screen capture.&lt;/li&gt;
&lt;li&gt;Sometimes the PiP plane records normally while in-page video stayed black for the same session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treat this as empirical behavior, not a guaranteed recipe. Extensions can alter DOM and composition in non-standard ways; Chromium updates and studio policy changes invalidate write-ups constantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why PiP can change what recorders see
&lt;/h2&gt;

&lt;p&gt;Protected playback pairs decryption with a display path that is supposed to keep cleartext frames out of buffers that ordinary screen grabbers read. For in-tab or some fullscreen presentations, the compositor tags that layer as non-capturable on supported pipelineshardware-bounded video overlays on some GPUs, tighter hand-off to the OS capture stack, and related platform contracts.&lt;/p&gt;

&lt;p&gt;Picture-in-Picture in Chromium-class browsers is not “the same widget moved.” The video is re-parented into a separate window or surface, and the browser runs a different code path for sizing, layers, and sometimes decode routing. When that path does not participate in the same protected-memory contract, or when playback is already on a Widevine L3-style software decode feeding ordinary GPU texturescapture tools may observe pixels again.&lt;/p&gt;

&lt;p&gt;The hand-wavy version you hear in forums (“hardware fullscreen vs software PiP”) points at a real idea*&lt;em&gt;different surfaces, different security tags&lt;/em&gt;*but oversimplifies how decode, scaling, and the OS compositor cooperate. The precise mechanics are proprietary and shift per release.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Typical goal&lt;/th&gt;
&lt;th&gt;What capture often sees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-page / some fullscreen paths&lt;/td&gt;
&lt;td&gt;Keep cleartext off general capture&lt;/td&gt;
&lt;td&gt;Black or masked video rectangle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PiP / alternate surface&lt;/td&gt;
&lt;td&gt;Usable floating window, sane GPU work&lt;/td&gt;
&lt;td&gt;Sometimes unprotected composition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table is debugging intuition, not a spec.&lt;/p&gt;




&lt;h2&gt;
  
  
  Widevine L3 vs L1 (and why 4K stays stubborn)
&lt;/h2&gt;

&lt;p&gt;Widevine levels roughly describe where keys and critical decrypt steps live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;L3&lt;/strong&gt;: Software CDM; decode may run without the strongest TEE binding. Studios often cap resolution or bitrate for L3-only clients. That looser chain is where more capture quirks tend to appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L1&lt;/strong&gt;: Hardware-backed; keys and decode touch secure silicon paths designed to resist exfiltration. 4K HDR titles frequently require L1, which is why PiP often stays black there even when 1080p briefly behaved differently on the same machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the pattern “720p or 1080p slips through, 4K does not” matches industry expectations: stronger policy rides stronger hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security vs usability
&lt;/h2&gt;

&lt;p&gt;PiP is a mainstream accessibility and multitasking feature. Shipping it without wrecking battery, latency, or OS integration forces tradeoffs. Whenever two composition paths exist (inline player vs out-of-tree PiP), security review has to show both respect studio rules. Gaps get patched when found; yesterday’s observation is not tomorrow’s interface contract.&lt;/p&gt;

&lt;p&gt;That is why “DRM is not always unbreakable” is the accurate headline: not because protection is fake, but because systems are large, layered, and always moving.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Black video on capture depends on which decoder and compositor path feeds the pixels you see. PiP can switch that path enough that some L3 streams surface in capture-visible buffers again until vendors tighten the chain. Treat observations as time-stamped engineering facts, not stable exploits, and read your streamer’s terms before you press record.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/drm-pip-loophole/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 4 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>drm</category>
      <category>widevine</category>
      <category>streaming</category>
      <category>webplatform</category>
    </item>
    <item>
      <title>Core Web Vitals and Lighthouse: What the Scores Mean</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 07:57:47 +0000</pubDate>
      <link>https://forem.com/helloashish99/core-web-vitals-and-lighthouse-what-the-scores-mean-36od</link>
      <guid>https://forem.com/helloashish99/core-web-vitals-and-lighthouse-what-the-scores-mean-36od</guid>
      <description>&lt;p&gt;Related: &lt;a href="https://renderlog.in/blog/browser-main-thread-rendering-pipeline/" rel="noopener noreferrer"&gt;Browser Rendering Pipeline&lt;/a&gt;: the underlying mechanics that most of these metrics are actually measuring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LCP, CLS, INP, TTFB&lt;/strong&gt; — the Core Web Vitals are precise technical measurements, not vague UX impressions. Each one captures a specific failure mode: slow resource loading, visual instability, interaction lag, server latency. Understanding what each metric measures at the spec level, and why Lighthouse lab scores diverge dramatically from real-user CrUX data, is what separates diagnosing actual regressions from chasing synthetic numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this covers:&lt;/strong&gt; The technical definition and measurement mechanics of each Core Web Vital, why lab and field data diverge, why a perfect Lighthouse score coexists with terrible INP, and the exact debugging workflow for each metric.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuim9wqghooa4xcy6bro2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuim9wqghooa4xcy6bro2.png" alt="Diagram explaining how reserved space, aspect ratio, and font metrics prevent Cumulative Layout Shift (CLS)." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  LCP: Largest Contentful Paint
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Largest Contentful Paint&lt;/strong&gt; measures the time from when the page starts loading to when the largest content element in the viewport is rendered. "Largest" is determined by the element's rendered size in the viewport, not its intrinsic size, not its file size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which elements qualify
&lt;/h3&gt;

&lt;p&gt;Not everything counts as an LCP candidate. The spec is specific:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element type&lt;/th&gt;
&lt;th&gt;Qualifies?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;image&amp;gt;&lt;/code&gt; inside SVG&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; poster image&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS &lt;code&gt;background-image&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes (as of recent spec updates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block-level elements with text&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline elements&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG shapes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The LCP candidate is updated as new elements render and become visible. If your page progressively reveals content (skeleton screen, then hero image, then content below the fold), the LCP timestamp updates each time a &lt;em&gt;larger&lt;/em&gt; element renders. The final LCP value is the last update before the user first interacts with the page (scroll, click, or keypress stops LCP measurement).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why images and text blocks differ
&lt;/h3&gt;

&lt;p&gt;An image LCP depends on &lt;strong&gt;network fetch time&lt;/strong&gt;  the browser has to download the image before it can paint it. A text block LCP depends only on &lt;strong&gt;CSS and font availability&lt;/strong&gt;, which is usually much faster. This is why a page with a large hero image almost always has worse LCP than a text-heavy landing page of equivalent visual weight.&lt;/p&gt;

&lt;p&gt;The most common LCP problem I see: a hero image that loads fast on a Lighthouse test (simulated throttled connection, nearby CDN edge) but loads in 5+ seconds for users on mobile in Southeast Asia because the image isn't on a CDN node close to them, or worse, it's not being served with proper &lt;code&gt;Cache-Control&lt;/code&gt; headers and gets fetched fresh every visit.&lt;/p&gt;

&lt;h3&gt;
  
  
  What invalidates LCP
&lt;/h3&gt;

&lt;p&gt;LCP measurement stops at the first user interaction. This is intentional  once a user starts scrolling or clicking, the "initial load" is considered complete. This means that if your largest element loads just &lt;em&gt;after&lt;/em&gt; a user's first touch, it won't be counted as LCP, which can make synthetic measurements look better than real experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  CLS: Cumulative Layout Shift
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cumulative Layout Shift&lt;/strong&gt; measures visual instability: how much page content shifts around unexpectedly after it first renders. The score is a unitless number calculated from a formula that captures both how much of the viewport shifted and how far elements moved.&lt;/p&gt;

&lt;h3&gt;
  
  
  The formula
&lt;/h3&gt;

&lt;p&gt;The score for each individual layout shift is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;layout shift score = impact fraction × distance fraction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Impact fraction&lt;/strong&gt; is the fraction of the viewport that was affected by unstable elements during the shift. If an element occupies 50% of the viewport and moves, the impact fraction is at least 0.5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distance fraction&lt;/strong&gt; is how far the element moved, as a fraction of the viewport's largest dimension. An element that moves 100px on a 800px viewport has a distance fraction of 0.125.&lt;/p&gt;

&lt;p&gt;So an element that occupies half the screen and moves 20% of the viewport height gives a shift score of &lt;code&gt;0.5 × 0.2 = 0.1&lt;/code&gt;  right at the "needs improvement" boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLS is cumulative&lt;/strong&gt;  all individual layout shift scores are added together, with a windowing algorithm that groups shifts within 5 seconds of each other (session windows). The final CLS is the score of the worst session window.&lt;/p&gt;

&lt;h3&gt;
  
  
  What causes CLS
&lt;/h3&gt;

&lt;p&gt;The most common offenders I've encountered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images without explicit dimensions.&lt;/strong&gt; When the browser doesn't know the aspect ratio of an image before it loads, it allocates zero space for it. When the image arrives, everything below it jumps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Bad: no dimensions, causes layout shift when image loads --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Good: aspect ratio known upfront --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dynamic content injection.&lt;/strong&gt; A cookie banner, notification bar, or ad that gets inserted above existing content after the page loads will shift everything below it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web fonts causing FOUT.&lt;/strong&gt; When a custom font loads and replaces the fallback font, if the two fonts have different metrics (line height, character width), text reflows. This is the "flash of unstyled text" causing layout shifts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Mitigates font-swap shifts by matching fallback metrics */&lt;/span&gt;
&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'MyFont'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/fonts/myfont.woff2')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'woff2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Don't swap if font isn't ready, no shift */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Debugging CLS
&lt;/h3&gt;

&lt;p&gt;The web-vitals library reports which elements shifted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="nf"&gt;onCLS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shifted elements:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// sources contains element references and rects&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome DevTools also has a "Layout Shift Regions" overlay under Rendering → Layout Shift Regions. Blue flashes show exactly which elements are shifting. This makes diagnosis extremely fast once you know it exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  INP: Interaction to Next Paint
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Interaction to Next Paint&lt;/strong&gt; replaced &lt;strong&gt;First Input Delay&lt;/strong&gt; (FID) as a Core Web Vital in March 2024. Understanding why requires understanding what was wrong with FID.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why FID was replaced
&lt;/h3&gt;

&lt;p&gt;FID measured only the &lt;strong&gt;input delay&lt;/strong&gt; component of the &lt;em&gt;first&lt;/em&gt; interaction on a page: the time from when the user first clicks/taps/presses to when the browser starts processing the event. This had two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It measured only the &lt;em&gt;first&lt;/em&gt; interaction, which is often before heavy JavaScript has fully loaded. Many apps had good FID but terrible interactivity for subsequent clicks.&lt;/li&gt;
&lt;li&gt;It measured only input &lt;em&gt;delay&lt;/em&gt;, not the full interaction duration. Even if the browser started processing immediately, if the event handler took 400ms, FID would show 0ms and consider it a success.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How INP works
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;INP&lt;/strong&gt; measures the &lt;strong&gt;full latency of all interactions&lt;/strong&gt; throughout the page visit: click, tap, and keyboard events. For each interaction, it measures the time from user input to the browser's next paint after processing. The INP score is the &lt;strong&gt;worst interaction&lt;/strong&gt; observed during the page visit (with some statistical smoothing: specifically, it's the 98th percentile if there are many interactions, to avoid outliers from accidental double-clicks).&lt;/p&gt;

&lt;p&gt;An interaction's latency has three components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interaction latency = input delay + processing time + presentation delay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input delay&lt;/strong&gt;: time from input event to when the browser starts running event handlers. Caused by long tasks blocking the main thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing time&lt;/strong&gt;: how long the event handlers themselves take to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation delay&lt;/strong&gt;: time for the browser to render and paint after handlers complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;INP targets: under 200ms is "good", 200–500ms "needs improvement", over 500ms is "poor."&lt;/p&gt;

&lt;h3&gt;
  
  
  What "next paint" means
&lt;/h3&gt;

&lt;p&gt;The "next paint" in INP is the first frame the browser can render &lt;em&gt;after&lt;/em&gt; the event has been fully processed. This means the browser has to complete layout and paint, not just run the JavaScript. A handler that does &lt;code&gt;setState()&lt;/code&gt; in React and triggers a massive re-render contributes all of that re-render time to INP.&lt;/p&gt;

&lt;p&gt;This is why INP is a much more honest signal than FID. A click that triggers a 600ms React reconciliation will show up as a 600ms INP, not a 0ms FID.&lt;/p&gt;




&lt;h2&gt;
  
  
  TTFB: Time to First Byte
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time to First Byte&lt;/strong&gt; measures the time from when the browser starts the navigation request to when it receives the first byte of the response. It's a server + network metric.&lt;/p&gt;

&lt;p&gt;What TTFB captures: server processing time (your Next.js SSR, your database query), DNS lookup, TCP connection, TLS handshake, and network transit.&lt;/p&gt;

&lt;p&gt;What TTFB does &lt;strong&gt;not&lt;/strong&gt; capture: anything that happens after the server starts streaming. A server that starts streaming HTML in 100ms but spends another 2 seconds building the response will look fast in TTFB but still deliver a slow page.&lt;/p&gt;

&lt;p&gt;TTFB targets: under 800ms is "good." This sounds loose, but global users on mobile networks with high latency to your origin can easily exceed this even if your server responds in 10ms, purely from network round trips.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lab vs field data: why they diverge
&lt;/h2&gt;

&lt;p&gt;This is the thing that burned us. &lt;strong&gt;Lab data&lt;/strong&gt; (Lighthouse, WebPageTest) runs in a controlled environment: a fixed device emulation, a fixed network speed, a server physically close to the testing infrastructure, and no real user behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field data&lt;/strong&gt; comes from real users. Google's &lt;strong&gt;CrUX (Chrome User Experience Report)&lt;/strong&gt; collects performance metrics from Chrome users who have opted in to usage statistics. PageSpeed Insights shows you CrUX data for your URL when it exists (it needs enough visits to have a statistically meaningful sample).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Lab (Lighthouse)&lt;/th&gt;
&lt;th&gt;Field (CrUX)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Emulated mid-range Android&lt;/td&gt;
&lt;td&gt;Real user devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Fixed throttled (4G simulation)&lt;/td&gt;
&lt;td&gt;Real network conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geography&lt;/td&gt;
&lt;td&gt;One location&lt;/td&gt;
&lt;td&gt;All user locations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User behavior&lt;/td&gt;
&lt;td&gt;No interactions&lt;/td&gt;
&lt;td&gt;Real clicks, scrolls, navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency to CDN&lt;/td&gt;
&lt;td&gt;Often very low&lt;/td&gt;
&lt;td&gt;Varies by user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data freshness&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;28-day rolling window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;28-day rolling window&lt;/strong&gt; is particularly important. If you deployed a performance regression two weeks ago, it's baked into your CrUX data. If you fixed it yesterday, the improvement won't show up in Search Console for weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 75th percentile rule
&lt;/h2&gt;

&lt;p&gt;Google doesn't use your &lt;em&gt;average&lt;/em&gt; Core Web Vitals score to determine your Search Console assessment. They use the &lt;strong&gt;75th percentile&lt;/strong&gt; of field data: the value that 75% of your page loads achieve or better.&lt;/p&gt;

&lt;p&gt;This matters enormously for INP. If 74% of your users have an INP under 200ms but 26% have an INP over 800ms (perhaps mobile users on slow devices running your heavy React app), your 75th percentile INP is over 200ms and you're not in the "good" zone even though most users are fine.&lt;/p&gt;

&lt;p&gt;This is by design. Google wants the score to reflect the experience of the bottom quartile of your users, not the median or average.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a perfect Lighthouse score coexists with terrible INP
&lt;/h2&gt;

&lt;p&gt;This was the most confusing part for our team to accept. Here's how it happens:&lt;/p&gt;

&lt;p&gt;Lighthouse runs with no user interactions beyond the page load. INP requires interactions. Lighthouse doesn't measure INP from actual clicks because there are no clicks in a lab test  it estimates it from &lt;strong&gt;Total Blocking Time&lt;/strong&gt; (TBT) and long tasks during load. This estimate can be wildly off.&lt;/p&gt;

&lt;p&gt;Our app had minimal load-time blocking: we code-split aggressively, deferred everything non-critical. Lighthouse loved it. But after hydration, when users actually clicked, our React event handlers triggered enormous re-renders. INP was 740ms in the field because we were doing 400ms of synchronous reconciliation on every button click. Lighthouse never saw this.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to actually measure each metric
&lt;/h2&gt;

&lt;p&gt;Don't rely only on Lighthouse. Use a layered approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// Report all Core Web Vitals to your analytics&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendToAnalytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'good', 'needs-improvement', 'poor'&lt;/span&gt;
    &lt;span class="na"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;navigationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigationType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Use sendBeacon so it fires even when page is unloading&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;onLCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendToAnalytics&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;onCLS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendToAnalytics&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendToAnalytics&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;onTTFB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendToAnalytics&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also observe INP directly using &lt;code&gt;PerformanceObserver&lt;/code&gt; if you want to capture interaction-level detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Capture slow interactions with the element that was clicked&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Slow interaction:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'click', 'keydown', etc.&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;durationThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tools and what they're good for:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse (DevTools)&lt;/td&gt;
&lt;td&gt;Lab&lt;/td&gt;
&lt;td&gt;Quick feedback during development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PageSpeed Insights&lt;/td&gt;
&lt;td&gt;Lab + Field&lt;/td&gt;
&lt;td&gt;Checking real user CrUX data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search Console Core Web Vitals&lt;/td&gt;
&lt;td&gt;Field&lt;/td&gt;
&lt;td&gt;28-day trend, page-group analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;web-vitals JS library&lt;/td&gt;
&lt;td&gt;Field (real users)&lt;/td&gt;
&lt;td&gt;Sending to your own analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PerformanceObserver&lt;/td&gt;
&lt;td&gt;Field (real users)&lt;/td&gt;
&lt;td&gt;Detailed interaction diagnostics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebPageTest&lt;/td&gt;
&lt;td&gt;Lab (multi-location)&lt;/td&gt;
&lt;td&gt;Geographic testing, filmstrip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Practical debugging workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For LCP problems:&lt;/strong&gt; open the Network panel, filter by Img, find your hero image. Check: is it preloaded? Is it on a CDN? Is it in WebP/AVIF format? Then open the Performance panel and look for the LCP marker. It shows you exactly which element was measured and when it rendered relative to navigation start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For CLS problems:&lt;/strong&gt; enable Layout Shift Regions in DevTools → Rendering. Then use &lt;code&gt;onCLS&lt;/code&gt; from web-vitals with the &lt;code&gt;entries.sources&lt;/code&gt; to identify the specific elements. Add explicit &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; to all images. Use &lt;code&gt;font-display: optional&lt;/code&gt; or size-adjusted fallback fonts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For INP problems:&lt;/strong&gt; record a Performance trace while clicking the button that feels slow. Look for long yellow JavaScript blocks in the main thread after your click. The call stack will show you exactly which function is taking 400ms. Then read the next post in this series on Long Tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For TTFB problems:&lt;/strong&gt; use WebPageTest's "Connection View" to see how much time is DNS, TCP, TLS, and server wait. If server wait is high, look at your SSR code, database queries, and edge caching configuration.&lt;/p&gt;

&lt;p&gt;The scores in Search Console are the destination. The tools above are how you navigate there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/core-web-vitals-lighthouse-explained/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 13 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>corewebvitals</category>
      <category>lighthouse</category>
      <category>lcp</category>
    </item>
    <item>
      <title>Why React Error Boundaries Are Still Class Components</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 06:57:46 +0000</pubDate>
      <link>https://forem.com/helloashish99/why-react-error-boundaries-are-still-class-components-110a</link>
      <guid>https://forem.com/helloashish99/why-react-error-boundaries-are-still-class-components-110a</guid>
      <description>&lt;p&gt;If a child component throws during render, React can &lt;strong&gt;unmount the whole subtree&lt;/strong&gt; beneath it. Related: &lt;a href="https://renderlog.in/blog/react-state-management-patterns/" rel="noopener noreferrer"&gt;React State Management Patterns&lt;/a&gt;—another area where the class-vs-hooks split shows up in practice.&lt;/p&gt;

&lt;p&gt;Without containment, one bad leaf can leave users staring at a blank page—the informal &lt;strong&gt;“white screen of death.”&lt;/strong&gt; &lt;strong&gt;React error boundaries&lt;/strong&gt; exist to &lt;strong&gt;catch rendering errors&lt;/strong&gt; and show a &lt;strong&gt;fallback UI&lt;/strong&gt; instead of tearing down the entire app.&lt;/p&gt;

&lt;p&gt;Most greenfield React code today is &lt;strong&gt;functional components&lt;/strong&gt; and &lt;strong&gt;hooks&lt;/strong&gt;. Error boundaries are the awkward exception: the &lt;strong&gt;supported API still lives on a class component&lt;/strong&gt;. This post explains &lt;strong&gt;why&lt;/strong&gt;, which lifecycle methods matter, &lt;strong&gt;what boundaries never catch&lt;/strong&gt;, and a &lt;strong&gt;minimal pattern&lt;/strong&gt; that keeps the rest of your tree function-based.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo2f3vcu1q6cqf4b8txrn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo2f3vcu1q6cqf4b8txrn.png" alt="Diagram showing a React component tree where an error boundary catches a throw in a child component and renders a fallback UI instead of crashing the whole tree." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What an error boundary actually is
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;error boundary&lt;/strong&gt; is a React component that implements a &lt;strong&gt;specific error-handling contract&lt;/strong&gt; with the reconciler. During a commit, if React hits an &lt;strong&gt;uncaught error&lt;/strong&gt; in certain phases of the tree below the boundary, it can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture&lt;/strong&gt; the error instead of propagating it to the root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; enough state to know a failure happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render a fallback&lt;/strong&gt; (or delegate to a parent boundary).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The feature is &lt;strong&gt;deliberately narrow&lt;/strong&gt;. It is not a general &lt;code&gt;try/catch&lt;/code&gt; for all JavaScript failures in your component tree—more on that below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why error boundaries are class components today
&lt;/h2&gt;

&lt;p&gt;React’s public API for this behavior is still expressed in terms of &lt;strong&gt;class lifecycle hooks&lt;/strong&gt; that do not have &lt;strong&gt;first-class hook equivalents&lt;/strong&gt; in React’s stable core:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lifecycle&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;static getDerivedStateFromError(error)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runs during render when a descendant throws. Returns an object to &lt;strong&gt;update state&lt;/strong&gt; from the error (for example, &lt;code&gt;{ hasError: true }&lt;/code&gt;) so the next render can branch to fallback UI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;componentDidCatch(error, info)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runs &lt;strong&gt;after&lt;/strong&gt; a commit. Use it for &lt;strong&gt;side effects&lt;/strong&gt;: logging to an APM, &lt;code&gt;console.error&lt;/code&gt;, reporting &lt;code&gt;componentStack&lt;/code&gt; from &lt;code&gt;info&lt;/code&gt;, avoiding duplicate reports, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Functional components cannot declare &lt;strong&gt;static&lt;/strong&gt; methods or these &lt;strong&gt;class-only lifecycle&lt;/strong&gt; entry points through &lt;code&gt;useEffect&lt;/code&gt;/&lt;code&gt;useState&lt;/code&gt; alone, because the reconciler invokes these hooks &lt;strong&gt;at specific times&lt;/strong&gt; in the &lt;strong&gt;error recovery path&lt;/strong&gt;—not as ordinary “effects after paint.” Until React exposes a &lt;strong&gt;blessed hook or component primitive&lt;/strong&gt; with the same semantics, the documented, stable way to author a boundary is a &lt;strong&gt;class&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the entire answer to “&lt;strong&gt;why are React error boundaries only class components?&lt;/strong&gt;” It is not ideology; it is &lt;strong&gt;API surface&lt;/strong&gt;. You can still &lt;strong&gt;wrap&lt;/strong&gt; function components—your app stays hooks-first; one small class (or a library wrapper) sits at the edge of a subtree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Minimal class boundary (copy-paste pattern)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ErrorInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;getDerivedStateFromError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;componentDidCatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ErrorInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage around &lt;strong&gt;functional&lt;/strong&gt; routes, widgets, or data-heavy subtrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Production tip:&lt;/strong&gt; pair &lt;code&gt;componentDidCatch&lt;/code&gt; with your &lt;strong&gt;logging pipeline&lt;/strong&gt; (Sentry, Datadog, OpenTelemetry, etc.) and include &lt;code&gt;info.componentStack&lt;/code&gt;—it shortens time-to-root-cause dramatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  What error boundaries do &lt;strong&gt;not&lt;/strong&gt; catch
&lt;/h2&gt;

&lt;p&gt;Treat this as a hard mental split, not a vague “maybe.” Do &lt;strong&gt;not&lt;/strong&gt; assume a boundary shields you from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event handler errors&lt;/strong&gt;  wrap synchronous handler code in &lt;code&gt;try/catch&lt;/code&gt;, or handle promise rejections from async handlers yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous errors&lt;/strong&gt; set &lt;strong&gt;after&lt;/strong&gt; render (&lt;code&gt;setTimeout&lt;/code&gt;, many &lt;code&gt;Promise&lt;/code&gt; chains, &lt;code&gt;fetch&lt;/code&gt; callbacks) unless they happen to throw &lt;strong&gt;during&lt;/strong&gt; a phase React is already turning into a render error boundary path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors in the boundary itself&lt;/strong&gt;  a broken fallback or a throw inside &lt;code&gt;getDerivedStateFromError&lt;/code&gt; does not magically self-heal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The React docs stress this split: &lt;strong&gt;error boundaries are for render-time failures in the tree below them&lt;/strong&gt;, not a substitute for normal &lt;strong&gt;defensive coding&lt;/strong&gt; in IO and events.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy: where to place boundaries
&lt;/h2&gt;

&lt;p&gt;Think in &lt;strong&gt;blast-radius units&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route-level&lt;/strong&gt;  one boundary per major page or lazy-loaded chunk so a bad panel does not blank the shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature-level&lt;/strong&gt;  isolate experimental or data-heavy widgets (charts, rich text, third-party embeds).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t over-nest&lt;/strong&gt;  too many stacked boundaries make UX and logging noisier; align with your design system’s &lt;strong&gt;fallback patterns&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Libraries such as &lt;strong&gt;&lt;code&gt;react-error-boundary&lt;/code&gt;&lt;/strong&gt; wrap the same lifecycle contract behind a &lt;strong&gt;declarative&lt;/strong&gt; API; under the hood the mechanics are still “&lt;strong&gt;class boundary semantics&lt;/strong&gt;,” not a different reconciler feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mental model checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Need to stop a render-time exception from killing the app?&lt;/strong&gt; Add a boundary &lt;strong&gt;above&lt;/strong&gt; the risky subtree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to catch a click-handler or failed &lt;code&gt;fetch&lt;/code&gt;?&lt;/strong&gt; Use &lt;strong&gt;&lt;code&gt;try/catch&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;Result&lt;/code&gt; types&lt;/strong&gt;, or &lt;strong&gt;&lt;code&gt;await&lt;/code&gt; in an &lt;code&gt;async&lt;/code&gt; event path&lt;/strong&gt;—not a boundary alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want hooks everywhere else?&lt;/strong&gt; Keep using them; &lt;strong&gt;one class file&lt;/strong&gt; (or library) at the edge is normal in modern &lt;strong&gt;React&lt;/strong&gt; codebases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If React adds a first-party hook or primitive with the same catch-and-recover semantics as class boundaries, you can adopt it incrementally behind the same placement ideas above. As of today, &lt;strong&gt;&lt;code&gt;getDerivedStateFromError&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;componentDidCatch&lt;/code&gt;&lt;/strong&gt; are still the supported surface for that behavior in app code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/why-react-error-boundaries-class-components/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 5 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>errors</category>
    </item>
    <item>
      <title>React State Management Compared: Redux vs Zustand vs Jotai vs Valtio</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 05:57:46 +0000</pubDate>
      <link>https://forem.com/helloashish99/react-state-management-compared-redux-vs-zustand-vs-jotai-vs-valtio-307m</link>
      <guid>https://forem.com/helloashish99/react-state-management-compared-redux-vs-zustand-vs-jotai-vs-valtio-307m</guid>
      <description>&lt;p&gt;Most teams pick &lt;strong&gt;React state libraries&lt;/strong&gt; from npm trends or conference hype. Also in this series: &lt;a href="https://renderlog.in/blog/why-react-error-boundaries-class-components/" rel="noopener noreferrer"&gt;Why React Error Boundaries Are Class Components&lt;/a&gt;: one of the few React APIs that still requires a class.&lt;/p&gt;

&lt;p&gt;That works until &lt;strong&gt;re-renders&lt;/strong&gt; spike, &lt;strong&gt;boilerplate&lt;/strong&gt; spreads, or &lt;strong&gt;onboarding&lt;/strong&gt; slows new hires. The useful split is not “which logo is popular” but &lt;strong&gt;which architecture&lt;/strong&gt; you are buying: a &lt;strong&gt;centralized store&lt;/strong&gt;, an &lt;strong&gt;atomic graph&lt;/strong&gt;, or a &lt;strong&gt;proxy snapshot&lt;/strong&gt; model.&lt;/p&gt;

&lt;p&gt;This post compares &lt;strong&gt;Redux&lt;/strong&gt; and &lt;strong&gt;Zustand&lt;/strong&gt; (centralized), &lt;strong&gt;Jotai&lt;/strong&gt; (atomic), and &lt;strong&gt;Valtio&lt;/strong&gt; (proxy)what each optimizes for, where it hurts, and how to choose without regret.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw09axcf84o90x0gjh3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw09axcf84o90x0gjh3g.png" alt="Architecture comparison of React state libraries: centralized store, atomic graph, and proxy snapshot models." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Centralized stores: Redux and Zustand
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Centralized state&lt;/strong&gt; means a &lt;strong&gt;single logical store&lt;/strong&gt; (or a small number of stores) holding domain data. Components &lt;strong&gt;select&lt;/strong&gt; slices and &lt;strong&gt;dispatch&lt;/strong&gt; updates through a defined path. The mental model matches the diagram above: &lt;strong&gt;global state sits above&lt;/strong&gt; tabs, routes, and feature panels so &lt;strong&gt;switching UI does not orphan data&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redux
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Redux&lt;/strong&gt; is the &lt;strong&gt;explicit, event-sourced&lt;/strong&gt; style: actions → reducers → predictable state. Ecosystem depth (&lt;strong&gt;Redux Toolkit&lt;/strong&gt;, &lt;strong&gt;RTK Query&lt;/strong&gt;, DevTools, middleware) is the main reason enterprises still standardize on it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Enforced data flow&lt;/strong&gt; is easy to review in PRs and teach in onboarding docs.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ceremony&lt;/strong&gt; without RTK; even with RTK, new devs must learn &lt;strong&gt;slices&lt;/strong&gt;, &lt;strong&gt;thunks&lt;/strong&gt;, or listeners.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Time-travel debugging&lt;/strong&gt; and structured logs map well to compliance-heavy environments.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Boilerplate pressure&lt;/strong&gt; returns if you avoid conventions and RTK patterns.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Large-team scalability&lt;/strong&gt; when you invest in patterns and codegen.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Selectors and memoization&lt;/strong&gt; still require discipline to avoid broad re-renders.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Zustand
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Zustand&lt;/strong&gt; keeps a &lt;strong&gt;minimal store API&lt;/strong&gt;: subscribe, getState, setStateoften without Providers. It is &lt;strong&gt;centralized&lt;/strong&gt; like Redux but &lt;strong&gt;defaults to less structure&lt;/strong&gt;, which speeds small and mid-size apps.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Tiny surface area&lt;/strong&gt;; hook-based reads feel native in &lt;strong&gt;React&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Conventions are yours&lt;/strong&gt;without guardrails, stores can become grab-bags.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Flexible middleware&lt;/strong&gt; (persist, immer, devtools) without a framework lock-in story.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Very large teams&lt;/strong&gt; may miss Redux’s &lt;strong&gt;uniform action/reducer&lt;/strong&gt; story for cross-repo training.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong fit when you want &lt;strong&gt;one obvious place&lt;/strong&gt; for cross-cutting client state.&lt;/td&gt;
&lt;td&gt;Deep &lt;strong&gt;async flows&lt;/strong&gt; still need clear patterns (no built-in “official” saga equivalent).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When centralized wins:&lt;/strong&gt; domain rules span many screens, &lt;strong&gt;several engineers&lt;/strong&gt; touch the same data, you need &lt;strong&gt;auditable updates&lt;/strong&gt;, or &lt;strong&gt;hydration/persistence&lt;/strong&gt; is first-class. &lt;strong&gt;Redux&lt;/strong&gt; leans &lt;strong&gt;enterprise process and tooling&lt;/strong&gt;; &lt;strong&gt;Zustand&lt;/strong&gt; leans &lt;strong&gt;velocity and low ceremony&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Atomic state: Jotai
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Jotai&lt;/strong&gt; models state as a &lt;strong&gt;graph of atoms&lt;/strong&gt;small units of state with &lt;strong&gt;explicit dependencies&lt;/strong&gt;. Derived data is &lt;strong&gt;atoms that read other atoms&lt;/strong&gt;. React subscribes &lt;strong&gt;per atom&lt;/strong&gt;, so components tend to re-render &lt;strong&gt;only when their atoms change&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Fine-grained subscriptions&lt;/strong&gt; reduce unnecessary renders without hand-tuned selectors everywhere.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Mental model shift&lt;/strong&gt;: thinking in graphs differs from “one big store object.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colocates &lt;strong&gt;derived state&lt;/strong&gt; as composition of atomssimilar to &lt;strong&gt;graphs in Recoil&lt;/strong&gt;’s family.&lt;/td&gt;
&lt;td&gt;Large &lt;strong&gt;imperative workflows&lt;/strong&gt; that touch many atoms at once need discipline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong for &lt;strong&gt;UI-local and shared&lt;/strong&gt; state mixed in one model.&lt;/td&gt;
&lt;td&gt;Team &lt;strong&gt;documentation&lt;/strong&gt; must explain atom layering to avoid spaghetti dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When atomic wins:&lt;/strong&gt; performance-sensitive UIs where &lt;strong&gt;isolate rerenders matter&lt;/strong&gt;, dashboards with &lt;strong&gt;many independent dimensions&lt;/strong&gt;, or when you prefer &lt;strong&gt;declarative dependency&lt;/strong&gt; wiring over manual memoized selectors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Proxy reactivity: Valtio
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Valtio&lt;/strong&gt; exposes &lt;strong&gt;mutable-looking plain objects&lt;/strong&gt; that are &lt;strong&gt;tracked with proxies&lt;/strong&gt;. You &lt;strong&gt;mutate properties&lt;/strong&gt;; the library resolves &lt;strong&gt;snapshots and subscriptions&lt;/strong&gt; for React. It feels closest to &lt;strong&gt;“just update my JS object.”&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Low cognitive overhead&lt;/strong&gt; if your team already thinks in mutable domain models.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Proxy mental model&lt;/strong&gt;: debugging requires understanding what is tracked vs plain objects.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excellent for &lt;strong&gt;simple reactive objects&lt;/strong&gt; and rapid prototyping.&lt;/td&gt;
&lt;td&gt;Some patterns (nested structures, cross-cutting immutability expectations) need &lt;strong&gt;explicit docs&lt;/strong&gt; on your team.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small API; pairs well with &lt;strong&gt;TypeScript&lt;/strong&gt; when patterns are stable.&lt;/td&gt;
&lt;td&gt;May clash with codebases that treat &lt;strong&gt;deep immutability&lt;/strong&gt; as a strict rule everywhere.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When proxy wins:&lt;/strong&gt; &lt;strong&gt;product and UX iteration speed&lt;/strong&gt; matter more than formal action logs, or you want &lt;strong&gt;colocated mutable models&lt;/strong&gt; with minimal glue code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture comparison at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Centralized (Redux / Zustand)&lt;/th&gt;
&lt;th&gt;Atomic (Jotai)&lt;/th&gt;
&lt;th&gt;Proxy (Valtio)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary lever&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One (or few) stores, explicit updates&lt;/td&gt;
&lt;td&gt;Per-atom subscriptions&lt;/td&gt;
&lt;td&gt;Mutable proxy + snapshots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strong for &lt;strong&gt;large&lt;/strong&gt; teams with conventions&lt;/td&gt;
&lt;td&gt;Strong when render cost is the bottleneck&lt;/td&gt;
&lt;td&gt;Strong for &lt;strong&gt;small&lt;/strong&gt; teams and fast iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Re-render story&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Relies on &lt;strong&gt;selectors&lt;/strong&gt;, &lt;code&gt;shallow&lt;/code&gt;, structure&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Built-in granularity&lt;/strong&gt; via atoms&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Property-level&lt;/strong&gt; tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redux steeper; Zustand gentle&lt;/td&gt;
&lt;td&gt;New graph model&lt;/td&gt;
&lt;td&gt;Familiar JS, proxy caveats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redux tooling gold standard&lt;/td&gt;
&lt;td&gt;Atom dependency graphs&lt;/td&gt;
&lt;td&gt;Snapshot / proxy awareness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Decision checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do multiple features read and write the same domain entities?&lt;/strong&gt; Prefer &lt;strong&gt;centralized&lt;/strong&gt; (Redux or Zustand) and document &lt;strong&gt;update paths&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is unnecessary re-rendering your main pain?&lt;/strong&gt; Prototype &lt;strong&gt;Jotai&lt;/strong&gt; or tighten &lt;strong&gt;selectors&lt;/strong&gt; in your current store before swapping everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you want the least abstraction over plain objects?&lt;/strong&gt; Try &lt;strong&gt;Valtio&lt;/strong&gt; in strict boundaries (one feature slice) and measure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is auditability or time-travel non-negotiable?&lt;/strong&gt; &lt;strong&gt;Redux DevTools&lt;/strong&gt; and explicit actions still earn their keep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Popularity is a lagging indicator; &lt;strong&gt;architecture fit&lt;/strong&gt; is leading. Match the library to &lt;strong&gt;how your team reasons about state&lt;/strong&gt;, not only to download countsand reserve the right to mix &lt;strong&gt;local &lt;code&gt;useState&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;React Query&lt;/strong&gt; for &lt;strong&gt;server state&lt;/strong&gt;, and one of these patterns for &lt;strong&gt;client-owned&lt;/strong&gt; complexity.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/react-state-management-patterns/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 5 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>statemanagement</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>How Google Maps Predicts Traffic in Real Time: Live Data and ETA Explained</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 04:57:45 +0000</pubDate>
      <link>https://forem.com/helloashish99/how-google-maps-predicts-traffic-in-real-time-live-data-and-eta-explained-bfl</link>
      <guid>https://forem.com/helloashish99/how-google-maps-predicts-traffic-in-real-time-live-data-and-eta-explained-bfl</guid>
      <description>&lt;p&gt;Open Google Maps during rush hour and a stretch of road turns red before you feel the brake lights stack up. That is not luck. It is telemetry, graph algorithms, historical baselines, and human reports fused into a near–real-time pipeline serving billions of clients.&lt;/p&gt;

&lt;p&gt;Google's public documentation stays high level; the full recipe is proprietary. The systems shape is well understood in the industry though: phones behave like moving probes, the road network is a weighted graph, and anomaly detection against historical curves is what turns "slow" into "worse than usual": the signal that routing should penalize an edge and suggest an alternative.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxm7s3reblychqc1nlqu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxm7s3reblychqc1nlqu.png" alt="Pipeline diagram for real-time traffic: GPS probes, road graph, historical baselines, and client map updates." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Crowdsourced probes: speed and direction at scale
&lt;/h2&gt;

&lt;p&gt;With location services active, devices can contribute aggregated, anonymized telemetry: speed and heading along map-matched road geometry. No engineering team publishes the exact threshold ("fifty phones at 5 km/h ⇒ red"). The principle is sufficient: on a given road segment, if many independent devices report low speeds relative to free flow, the live speed layer for that polyline drops, congestion color escalates, and ETA models consume the update.&lt;/p&gt;

&lt;p&gt;Latency targets are tight because routing products compete on freshness: stale traffic is worse than noisy traffic for user trust.&lt;/p&gt;

&lt;p&gt;Privacy is handled through aggregation, sampling, retention limits, and noise. The product promise is patterns, not surveillance records of individual drivers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The road network as a graph
&lt;/h2&gt;

&lt;p&gt;Map vendors treat the digitized road network as a graph: intersections and merge points are nodes; directed edges carry length, speed limit, road class, turn restrictions, and time-dependent costs derived from live telemetry.&lt;/p&gt;

&lt;p&gt;When one edge jams, shortest-path queries care about more than that edge alone: downstream capacity, spillback, turn pockets, and alternative arterials create ripple effects. Production systems maintain live travel times per segment and periodically reoptimize routes for active users. The "AI" label in marketing usually refers to ML-weighted fusion of sensor inputs; hand-tuned rules still exist in the mix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Historical curves: "slow" versus "wrong slow"
&lt;/h2&gt;

&lt;p&gt;Monday 9:00 AM is supposed to be slow on commuter corridors. Maps keeps long-horizon baselines: historical speed distributions by time-of-day and day-of-week.&lt;/p&gt;

&lt;p&gt;When the current median speed on a segment diverges from baseline more than noise explains, the system flags an exception: a crash, closure, weather event, or demand spike. That is the moment rerouting nudges users onto parallel paths before the jam propagates. Cold starts (new roads) and holidays are harder; less history means wider confidence intervals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Waze and human-in-the-loop incidents
&lt;/h2&gt;

&lt;p&gt;After Google acquired Waze, user-reported incidents (stalled vehicle, object on road, police presence) became an additional sensor layer. Reports are cheap to create and noisy; fusion with GPS speed collapse and duplicate detection is how a report becomes a trusted incident polygon within seconds. Confirmation from independent probes reduces false positives that would erode trust.&lt;/p&gt;

&lt;p&gt;The comparison to fixed traffic cameras is about sensor latency, not competence: phones outnumber fixed cameras on most road networks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scale, latency, and honest caveats
&lt;/h2&gt;

&lt;p&gt;Moving probe streams through ingest, map matching, aggregation, ML scoring, and tile updates for clients is a distributed systems problem. Sub-minute freshness on busy grids is the product standard users feel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage is thinner on rural roads&lt;/strong&gt; and sparse at off-peak hours; congestion colors are noisier there. Any telemetry stack can bias toward device-rich demographics and routes favored by rideshare drivers. The specific models and thresholds are trade secrets. This post reflects systems intuition, not a leaked design doc.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Google Maps often "sees" congestion before you because many devices are already measuring it, the road mesh is a live graph, history separates routine slow from true incidents, and Waze-style reports accelerate verification. The engineering advantage is probe density, graph data, and pipelines built for low latency at continental scale, not a single clever model.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/google-maps-traffic-live-prediction/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 4 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlemaps</category>
      <category>traffic</category>
      <category>gps</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>The 16.6ms Frame Budget: Why Fast Loads Still Feel Slow</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 03:38:50 +0000</pubDate>
      <link>https://forem.com/helloashish99/the-166ms-frame-budget-why-fast-loads-still-feel-slow-2d70</link>
      <guid>https://forem.com/helloashish99/the-166ms-frame-budget-why-fast-loads-still-feel-slow-2d70</guid>
      <description>&lt;p&gt;Related: &lt;a href="https://renderlog.in/blog/why-css-never-matches-figma/" rel="noopener noreferrer"&gt;Why CSS Never Matches Figma&lt;/a&gt; which is another place where the browser's rendering pipeline creates unexpected gaps.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;60Hz&lt;/strong&gt;, the browser has &lt;strong&gt;~16.67ms per frame&lt;/strong&gt; for all JavaScript execution, style calculation, layout, paint, and compositing — combined. This is not a soft guideline. It is a hard physical constraint set by the display's refresh schedule. Miss the deadline and the display repeats the previous frame. Miss it consistently and users perceive jank regardless of what Lighthouse reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this covers:&lt;/strong&gt; Where the 16.6ms number comes from, what work must fit inside one frame, why good Lighthouse scores coexist with bad runtime performance, and why high-frequency data UIs are the most unforgiving stress test for this budget.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl0v7f91p5fwgyp5lml59.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl0v7f91p5fwgyp5lml59.png" alt="Diagram of the core browser rendering pipeline and how work must fit within the per-frame budget at 60Hz." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where 16.6 ms comes from
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;60 Hz&lt;/strong&gt; panel asks for &lt;strong&gt;60 frames per second&lt;/strong&gt;. One second divided by 60 is &lt;strong&gt;≈16.67 ms&lt;/strong&gt; per frameoften rounded to &lt;strong&gt;16.6 ms&lt;/strong&gt; in conversation.&lt;/p&gt;

&lt;p&gt;That number is not a browser setting; it is &lt;strong&gt;physics of the refresh schedule&lt;/strong&gt;. If the main thread and compositor together cannot produce a new frame in time, the display &lt;strong&gt;repeats the previous one&lt;/strong&gt;: you have dropped a frame. Enough drops in a row read as &lt;strong&gt;jank&lt;/strong&gt;, &lt;strong&gt;stutter&lt;/strong&gt;, or “&lt;strong&gt;laggy&lt;/strong&gt;” UI, even when &lt;strong&gt;First Contentful Paint&lt;/strong&gt; and &lt;strong&gt;Time to Interactive&lt;/strong&gt; look fine on a cold load.&lt;/p&gt;




&lt;h2&gt;
  
  
  What must fit inside one frame
&lt;/h2&gt;

&lt;p&gt;On the &lt;strong&gt;main thread&lt;/strong&gt;, a heavy tick might include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript:&lt;/strong&gt;  event handlers, your &lt;strong&gt;React&lt;/strong&gt; reconciler, data transforms, garbage collection spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Style:&lt;/strong&gt;  matching selectors and computing &lt;strong&gt;layout-affecting&lt;/strong&gt; properties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout:&lt;/strong&gt;  calculating geometry when something actually &lt;strong&gt;moves&lt;/strong&gt; or &lt;strong&gt;sizes&lt;/strong&gt; (cheap property changes can skip this; wide invalidations cannot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paint:&lt;/strong&gt;  rasterizing layers; some effects force more work than others.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;strong&gt;compositor&lt;/strong&gt; then assembles &lt;strong&gt;layers&lt;/strong&gt; for the GPU. Work that stays &lt;strong&gt;compositor-only&lt;/strong&gt; (for example, many &lt;strong&gt;&lt;code&gt;transform&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;opacity&lt;/code&gt;&lt;/strong&gt; animations) is how sites keep motion smoothbut &lt;strong&gt;parsing a giant JSON tick&lt;/strong&gt;, &lt;strong&gt;rebuilding a chart&lt;/strong&gt;, or &lt;strong&gt;mounting hundreds of rows&lt;/strong&gt; still tends to touch &lt;strong&gt;JS + layout + paint&lt;/strong&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; the &lt;strong&gt;16.6 ms budget&lt;/strong&gt; is a &lt;strong&gt;ceiling for all of that&lt;/strong&gt;, not “JavaScript time” alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why good Lighthouse scores can hide the problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lighthouse&lt;/strong&gt; and similar audits focus heavily on &lt;strong&gt;load&lt;/strong&gt; and &lt;strong&gt;lab&lt;/strong&gt; conditions: bundle size, critical path, caching, LCP, CLS in synthetic runs. They are invaluable for &lt;strong&gt;shipping fast first paint&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They are &lt;strong&gt;easy to misread&lt;/strong&gt; as “performance is solved” because &lt;strong&gt;runtime&lt;/strong&gt; pain shows up when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket&lt;/strong&gt; or &lt;strong&gt;SSE&lt;/strong&gt; pushes &lt;strong&gt;dozens of updates per second&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Each update triggers &lt;strong&gt;state&lt;/strong&gt; changes that &lt;strong&gt;re-render&lt;/strong&gt; large subtrees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Charts&lt;/strong&gt; reconcile thousands of points &lt;strong&gt;on the main thread&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The user &lt;strong&gt;scrolls&lt;/strong&gt; or &lt;strong&gt;zooms&lt;/strong&gt; while &lt;strong&gt;new ticks&lt;/strong&gt; land.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly the shape of a &lt;strong&gt;trading&lt;/strong&gt; or &lt;strong&gt;operations dashboard&lt;/strong&gt;: &lt;strong&gt;high-frequency&lt;/strong&gt; mutations plus &lt;strong&gt;dense&lt;/strong&gt; UI. &lt;strong&gt;Load&lt;/strong&gt; metrics stay green; &lt;strong&gt;frame time&lt;/strong&gt; spikes in &lt;strong&gt;production&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-time data and the main thread
&lt;/h2&gt;

&lt;p&gt;When every tick runs through &lt;strong&gt;React &lt;code&gt;setState&lt;/code&gt;&lt;/strong&gt; (or store writes that fan out to hooks), you pay &lt;strong&gt;pure JS&lt;/strong&gt; and often &lt;strong&gt;layout&lt;/strong&gt;. If a single burst exceeds &lt;strong&gt;~16.6 ms&lt;/strong&gt;, you &lt;strong&gt;miss the next v-sync&lt;/strong&gt;. Miss often enough and users perceive &lt;strong&gt;drag&lt;/strong&gt; even if CPU usage “looks low” averaged over a second.&lt;/p&gt;

&lt;p&gt;Patterns that help &lt;strong&gt;stay inside the budget&lt;/strong&gt; (none are magic; all need measurement):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Throttle / batch&lt;/strong&gt; updates&lt;/td&gt;
&lt;td&gt;Collapse 50 ticks into one &lt;strong&gt;RAF&lt;/strong&gt;-aligned commit per frame.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Isolate subscriptions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keep &lt;strong&gt;fine-grained&lt;/strong&gt; listeners so unrelated panels do not re-render.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Virtualize lists&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Render &lt;strong&gt;viewport rows&lt;/strong&gt; only for order books and tape-style feeds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Move work off-thread&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Web Workers&lt;/strong&gt; or &lt;strong&gt;WASM&lt;/strong&gt; for parsing and aggregation; ship &lt;strong&gt;snapshots&lt;/strong&gt; to UI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prefer cheap motion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Animate &lt;strong&gt;&lt;code&gt;transform&lt;/code&gt;/&lt;code&gt;opacity&lt;/code&gt;&lt;/strong&gt;; avoid &lt;strong&gt;layout-thrashing&lt;/strong&gt; read/write loops.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Profiling beats guessing: &lt;strong&gt;Performance&lt;/strong&gt; panel in Chrome, &lt;strong&gt;React Profiler&lt;/strong&gt;, and &lt;strong&gt;Long Task&lt;/strong&gt; marks tell you whether you are fighting &lt;strong&gt;JS&lt;/strong&gt;, &lt;strong&gt;layout&lt;/strong&gt;, or &lt;strong&gt;paint&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mental model checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;60 Hz ⇒ ~16.6 ms&lt;/strong&gt; per opportunity to show a &lt;strong&gt;new&lt;/strong&gt; frame; there is no secret “extra time.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast load ≠ smooth runtime&lt;/strong&gt;; &lt;strong&gt;streaming&lt;/strong&gt; UIs punish &lt;strong&gt;per-tick&lt;/strong&gt; cost on the &lt;strong&gt;main thread&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboards&lt;/strong&gt; and &lt;strong&gt;trading&lt;/strong&gt; surfaces are &lt;strong&gt;stress tests&lt;/strong&gt; Optimize them like &lt;strong&gt;games&lt;/strong&gt;: &lt;strong&gt;budget&lt;/strong&gt;, &lt;strong&gt;batch&lt;/strong&gt;, &lt;strong&gt;measure&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Runtime performance is not a different hobby from &lt;strong&gt;web performance&lt;/strong&gt;; it is the same discipline applied &lt;strong&gt;after&lt;/strong&gt; the first paint. Once you internalize the &lt;strong&gt;frame budget&lt;/strong&gt;, “&lt;strong&gt;why does this feel slow?&lt;/strong&gt;” stops being mysterious and becomes a &lt;strong&gt;timeline&lt;/strong&gt; question.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/16ms-frame-budget-60fps/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 5 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>rendering</category>
    </item>
    <item>
      <title>Why CSS Never Matches Figma: Browser vs Canvas Pipelines</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 03:37:50 +0000</pubDate>
      <link>https://forem.com/helloashish99/why-css-never-matches-figma-browser-vs-canvas-pipelines-2a54</link>
      <guid>https://forem.com/helloashish99/why-css-never-matches-figma-browser-vs-canvas-pipelines-2a54</guid>
      <description>&lt;p&gt;Open any Figma file. Look at those perfectly smooth corners, the glass blur with real depth, the stroke that sits cleanly inside the element without touching the spacing.&lt;/p&gt;

&lt;p&gt;Now build it in CSS.&lt;/p&gt;

&lt;p&gt;It’s close. But something is always slightly &lt;em&gt;off&lt;/em&gt;. The corners feel harder. The blur is flat. The border is eating into your padding.&lt;/p&gt;

&lt;p&gt;You’ve probably blamed yourself: wrong tool version, wrong browser, not enough CSS tricks. The real reason is simpler: &lt;strong&gt;Figma and your browser are two different rendering engines running different math.&lt;/strong&gt; Some things Figma draws don’t exist in CSS yet. Some things look the same but work differently under the hood.&lt;/p&gt;

&lt;p&gt;This post breaks down how each engine works, where they diverge, and what that means for every design-to-code handoff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmnbdy3cau30vjc89d75e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmnbdy3cau30vjc89d75e.png" alt="Diagram of the Figma canvas pipeline: WASM, GPU shaders, and vector rasterization versus the browser CSS layout and paint model." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How Chrome turns CSS into pixels
&lt;/h2&gt;

&lt;p&gt;When you write CSS and open it in Chrome, &lt;strong&gt;six things happen in sequence&lt;/strong&gt; for every frame and every element.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Parse
&lt;/h3&gt;

&lt;p&gt;Chrome reads your HTML and builds the &lt;strong&gt;DOM&lt;/strong&gt; (a tree of every element). In parallel it reads CSS and builds the &lt;strong&gt;CSSOM&lt;/strong&gt; (a tree of every style rule). Those two trees are combined into the &lt;strong&gt;render tree&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Style
&lt;/h3&gt;

&lt;p&gt;Chrome walks every element and decides which CSS rules apply. It resolves specificity, inheritance, and the cascade. Every node ends up with &lt;strong&gt;computed styles&lt;/strong&gt;; each property has an exact value.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Layout
&lt;/h3&gt;

&lt;p&gt;Chrome figures out &lt;strong&gt;where&lt;/strong&gt; each element sits and &lt;strong&gt;how much space&lt;/strong&gt; it needs. This step is expensive. Change one width and Chrome may recalculate layout for a large subtree. That’s what people mean by &lt;strong&gt;reflow&lt;/strong&gt;, and why it hurts performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Paint
&lt;/h3&gt;

&lt;p&gt;Chrome records &lt;strong&gt;drawing instructions&lt;/strong&gt;, not pixels yet: things like “fill this rectangle,” “draw this text run.” Order follows the CSS painting spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Composite
&lt;/h3&gt;

&lt;p&gt;The page is split into &lt;strong&gt;layers&lt;/strong&gt; and sent to the &lt;strong&gt;GPU&lt;/strong&gt;. The GPU composites layers into the bitmap you see. Properties like &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; can often skip heavy work earlier in the pipeline. That’s why they’re cheap to animate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The crucial idea:&lt;/strong&gt; every CSS property is implemented with math (&lt;strong&gt;fixed by web standards&lt;/strong&gt;) (W3C / CSS WG). You write &lt;code&gt;border-radius: 40px&lt;/code&gt;, and you get a circular arc, because the spec says so. You write &lt;code&gt;backdrop-filter: blur(10px)&lt;/code&gt;, and you get a &lt;strong&gt;Gaussian blur&lt;/strong&gt; on the spec’s terms. You can’t extend that math from your stylesheet; browsers implement one interoperable definition.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How Figma draws outside the DOM
&lt;/h2&gt;

&lt;p&gt;Figma made a different bet when it launched in &lt;strong&gt;2015&lt;/strong&gt;: the canvas is &lt;strong&gt;not&lt;/strong&gt; HTML and CSS.&lt;/p&gt;

&lt;h3&gt;
  
  
  C++ / Rust core
&lt;/h3&gt;

&lt;p&gt;Figma maintains its &lt;strong&gt;own document model&lt;/strong&gt; and &lt;strong&gt;scene graph&lt;/strong&gt;: a tree of layers, effects, and constraints. When you nudge a frame or tweak corner smoothing, that graph updates in a &lt;strong&gt;compiled native/WASM layer&lt;/strong&gt;, not in ad-hoc DOM updates from your UI JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebAssembly
&lt;/h3&gt;

&lt;p&gt;The renderer runs in the browser at &lt;strong&gt;near-native speed&lt;/strong&gt;. That’s how huge files with thousands of layers stay usable. The heavy work isn’t “the browser laying out divs,” it’s Figma’s code inside WASM.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebGL / WebGPU
&lt;/h3&gt;

&lt;p&gt;Figma walks its scene graph and issues &lt;strong&gt;raw GPU draw calls&lt;/strong&gt;. Effects are often &lt;strong&gt;custom shaders&lt;/strong&gt; (GLSL / WGSL): the math for corner smoothing, glass, blend modes, etc. is &lt;strong&gt;whatever Figma ships&lt;/strong&gt;, not “whatever all browsers agreed to implement.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Screen
&lt;/h3&gt;

&lt;p&gt;The GPU composites and displays the canvas. &lt;strong&gt;Chrome is not rendering Figma’s pixels.&lt;/strong&gt; It’s hosting a surface where Figma’s engine draws.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; Figma can ship a new visual primitive by writing shader + scene-graph logic and releasing. &lt;strong&gt;No multi-year standards track.&lt;/strong&gt; No waiting for Safari and Firefox to match. That freedom is exactly why mocks can outpace CSS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where design-to-code breaks: property by property
&lt;/h2&gt;

&lt;p&gt;Once you see both pipelines, mismatches stop feeling mysterious.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cornerSmoothing&lt;/code&gt; (Figma) vs &lt;code&gt;border-radius&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Figma can use a &lt;strong&gt;superellipse&lt;/strong&gt;-style curve, where curvature ramps smoothly into the corner instead of a hard hand-off. CSS &lt;code&gt;border-radius&lt;/code&gt; uses &lt;strong&gt;circular arcs&lt;/strong&gt;; you can get a subtle “kink” at the join. (Apple’s app-icon shape (is the famous real-world parallel).) There is W3C interest (&lt;code&gt;corner-shape&lt;/code&gt;, etc.), but &lt;strong&gt;nothing you can rely on in production everywhere yet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; SVG squircle paths, &lt;code&gt;clip-path&lt;/code&gt;, or tools like &lt;strong&gt;figma-squircle&lt;/strong&gt;, and accept some authoring cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stroke alignment vs &lt;code&gt;border&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;In Figma, &lt;strong&gt;inside / center / outside&lt;/strong&gt; stroke keeps geometry predictable relative to the frame. With &lt;code&gt;box-sizing: border-box&lt;/code&gt;, a CSS &lt;strong&gt;&lt;code&gt;border&lt;/code&gt; lives inside the box&lt;/strong&gt; you sized. It isn’t a free-floating “outside stroke” the same way. A &lt;strong&gt;2px inside stroke in Figma is not identical&lt;/strong&gt; to &lt;code&gt;border: 2px solid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; &lt;code&gt;box-shadow&lt;/code&gt; / &lt;code&gt;inset&lt;/code&gt; rings, extra wrappers, or explicit dimensions that &lt;em&gt;include&lt;/em&gt; the border math you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glass vs &lt;code&gt;backdrop-filter&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Figma &lt;strong&gt;glass&lt;/strong&gt; is often a &lt;strong&gt;physically inspired&lt;/strong&gt; stack (refraction, highlights, depth cues) implemented in shaders. CSS &lt;code&gt;backdrop-filter: blur()&lt;/code&gt; is a &lt;strong&gt;Gaussian-style blur&lt;/strong&gt; (and friends) on underlying pixels: powerful, but &lt;strong&gt;not the same light model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; layered &lt;code&gt;backdrop-filter&lt;/code&gt;, shadows, gradients, and aim for &lt;strong&gt;convincing&lt;/strong&gt;, not pixel-perfect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple fills
&lt;/h3&gt;

&lt;p&gt;Figma stacks &lt;strong&gt;many fills&lt;/strong&gt; on one layer, each with opacity and blend mode. In CSS you mostly compose &lt;strong&gt;&lt;code&gt;background&lt;/code&gt;&lt;/strong&gt; layers, with &lt;strong&gt;&lt;code&gt;background-color&lt;/code&gt;&lt;/strong&gt; at the bottom of that stack, so the model is flatter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; extra DOM (&lt;code&gt;::before&lt;/code&gt;, &lt;code&gt;::after&lt;/code&gt;, stacked children) to fake additional “fills.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Negative gap (Auto Layout) vs CSS &lt;code&gt;gap&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Figma Auto Layout can use &lt;strong&gt;negative gap&lt;/strong&gt; for deliberate overlap (avatars, stacks). CSS &lt;strong&gt;&lt;code&gt;gap&lt;/code&gt; cannot be negative&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; negative &lt;strong&gt;margins&lt;/strong&gt; on children. That works, but you’re outside the same mental model as Auto Layout; test breakpoints carefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plus Lighter vs &lt;code&gt;mix-blend-mode&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Figma’s &lt;strong&gt;Plus Lighter&lt;/strong&gt; is &lt;strong&gt;not&lt;/strong&gt; a 1:1 alias of a single CSS &lt;code&gt;mix-blend-mode&lt;/code&gt;. The compositing math is Figma’s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; approximate with something like &lt;code&gt;mix-blend-mode: screen&lt;/code&gt; and &lt;strong&gt;tune opacity&lt;/strong&gt; by eye.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the gap is structural (specs vs product shaders)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CSS is a shared contract.&lt;/strong&gt; New visual power means proposals, implementer consensus, shipping in multiple engines, and &lt;strong&gt;years&lt;/strong&gt; of iteration. That friction is &lt;em&gt;why&lt;/em&gt; your stylesheet behaves predictably on phones, kiosks, and four different browsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figma is a product renderer.&lt;/strong&gt; New visuals ship when the team &lt;strong&gt;writes the shader&lt;/strong&gt; and merges the release.&lt;/p&gt;

&lt;p&gt;Neither model is “wrong.” &lt;strong&gt;They’re optimized for different goals:&lt;/strong&gt; interoperability vs. design-tool expressiveness. Pretending they’re equivalent on every frame is where handoff pain comes from.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handoff checklist: sanity-check the file before CSS
&lt;/h2&gt;

&lt;p&gt;The gap isn’t something you “fix” with more clever CSS alone. It’s &lt;strong&gt;architectural&lt;/strong&gt;. It &lt;em&gt;is&lt;/em&gt; manageable.&lt;/p&gt;

&lt;p&gt;Before you write markup, skim the file for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corner smoothing above 0%?&lt;/strong&gt; Plan for squircle/&lt;code&gt;clip-path&lt;/code&gt; workarounds, not vanilla &lt;code&gt;border-radius&lt;/code&gt; alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outside / center stroke?&lt;/strong&gt; Don’t assume &lt;code&gt;border&lt;/code&gt; is a drop-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glass / heavy blur stacks?&lt;/strong&gt; Expect &lt;code&gt;backdrop-filter&lt;/code&gt; to be a &lt;strong&gt;rough&lt;/strong&gt; match, not a clone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple fills on one layer?&lt;/strong&gt; Plan extra elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative Auto Layout gap?&lt;/strong&gt; Plan a margin-based layout, not pure &lt;code&gt;gap&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have the conversation early: not “impossible,” but &lt;strong&gt;“native on the web / workaround, or intentional approximation.”&lt;/strong&gt; That clarity saves hours on every project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Figma features with no production CSS twin (today)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cornerSmoothing&lt;/code&gt; · stroke alignment models beyond &lt;code&gt;border&lt;/code&gt; · full &lt;strong&gt;glassFill&lt;/strong&gt; fidelity · unconstrained &lt;strong&gt;multipleFills&lt;/strong&gt; on one node · &lt;strong&gt;negativeGap&lt;/strong&gt; in flex/grid · some advanced &lt;strong&gt;layerBlur&lt;/strong&gt; looks · &lt;strong&gt;plusLighterBlend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some of this is &lt;strong&gt;moving&lt;/strong&gt; (&lt;code&gt;corner-shape&lt;/code&gt;, Houdini in Chrome in places). The gap is real &lt;strong&gt;now&lt;/strong&gt; and will &lt;strong&gt;shrink slowly&lt;/strong&gt;, and it’s worth tracking if you live in design systems.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Engineer’s breakdown of two pipelines, so your next build starts with the right expectations, not the wrong guilt.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/why-css-never-matches-figma/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 7 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>figma</category>
      <category>css</category>
      <category>designhandoff</category>
      <category>frontend</category>
    </item>
    <item>
      <title>DRM Explained: Why JioHotstar Goes Black When You Screen Share</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Wed, 08 Apr 2026 17:54:08 +0000</pubDate>
      <link>https://forem.com/helloashish99/drm-explained-why-jiohotstar-goes-black-when-you-screen-share-380</link>
      <guid>https://forem.com/helloashish99/drm-explained-why-jiohotstar-goes-black-when-you-screen-share-380</guid>
      <description>&lt;p&gt;If you try to screen share or record a JioHotstar cricket stream, the video often goes pitch black, while playback controls, the app chrome, and system navigation still look normal. That split is the giveaway: the wall is not around the whole phone. It sits between your capture API and the decrypted picture.&lt;/p&gt;

&lt;p&gt;This post unpacks &lt;strong&gt;Digital Rights Management (DRM)&lt;/strong&gt; as an engineering system: Encrypted Media Extensions (EME) on the web, Content Decryption Modules (CDMs) such as Widevine, and hardware-backed paths on modern phones (Trusted Execution Environment, secure video output).&lt;/p&gt;

&lt;p&gt;Also see: &lt;a href="https://renderlog.in/blog/drm-pip-loophole/" rel="noopener noreferrer"&gt;PiP Loophole: Why DRM Is Not Always Unbreakable&lt;/a&gt;, when Picture-in-Picture sometimes bypasses this protection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvqu1bp79pc49c0prllg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvqu1bp79pc49c0prllg.png" alt="Diagram of the secure compositor path for DRM video: decrypted frames stay on protected surfaces away from normal screen capture." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What you are actually seeing
&lt;/h2&gt;

&lt;p&gt;Screen capture reads from the composition path the OS exposes to recorders and mirroring tools. DRM-protected premium video is composited through a protected surface: the decoder and GPU cooperate so that plaintext frames either never land in CPU-accessible buffers, or are flagged so capture returns empty, black, or static for that layer only.&lt;/p&gt;

&lt;p&gt;That is why screenshots sometimes show UI perfectly while the video rectangle is blank, exactly the behavior you see in the split image above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Web playback: Encrypted Media Extensions (EME)
&lt;/h2&gt;

&lt;p&gt;In the browser, &lt;strong&gt;EME&lt;/strong&gt; is the W3C API surface that connects JavaScript to a CDM. Your app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receives encrypted media (often CENC or vendor-specific packaging).&lt;/li&gt;
&lt;li&gt;Uses EME to create &lt;code&gt;MediaKeys&lt;/code&gt; sessions and exchange license data with a license server.&lt;/li&gt;
&lt;li&gt;Never receives decrypted samples as ordinary &lt;code&gt;ArrayBuffer&lt;/code&gt; objects in page memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The browser negotiates with the CDM, a sandboxed or vendor component, depending on OS and Widevine level. Decryption and key material stay on the CDM side of the boundary; the page just drives playback. Your JavaScript code never "gets" raw frames in a way that can be copied to canvas or piped to a recorder without violating the security model.&lt;/p&gt;

&lt;p&gt;Native apps like JioHotstar use the same conceptual split (Android MediaDrm and Widevine-class stacks), not the DOM API literally, but the same job: keys and decryption happen outside normal app memory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content Decryption Modules and Widevine levels
&lt;/h2&gt;

&lt;p&gt;A CDM (for example Google Widevine) implements license handling and decryption policy. Implementations are graded: conceptually L3 software vs L1 hardware on many devices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;L3&lt;/strong&gt;: Software CDM. Decode runs without strong TEE binding. Studios often cap resolution at 720p or 1080p for L3-only clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L1&lt;/strong&gt;: Hardware-backed. Keys and decode touch secure silicon paths designed to resist exfiltration. 4K HDR titles typically require L1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That stack exists because broadcast and studio deals are priced on windowing and anti-redistribution. The client is treated as hostile; DRM is the contractual and technical compromise that lets live sports and premium content stream on commodity hardware at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phones, TEE, and the black rectangle in screen recorders
&lt;/h2&gt;

&lt;p&gt;On many modern phones, a high Widevine level ties decoding and output to a trusted path: a TEE or similar isolation holds key operations so the main OS cannot read protected plane pixels like a normal texture. When Screen Record requests frame data, the compositor omits or masks that layer. You see black, while unprotected UI layers (status bar, player controls) composite normally.&lt;/p&gt;

&lt;p&gt;This is not a bug in your screen recorder. It is working as designed for rights-managed content.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DRM raises the bar for casual HD ripping from client devices. It does not make piracy mathematically impossible; attacks shift to HDMI re-encode, CAM rips, leaks from insider sources, etc.&lt;/li&gt;
&lt;li&gt;Policies differ by platform, title, and studio rules. Some tiers allow limited mirroring on approved receivers.&lt;/li&gt;
&lt;li&gt;Fair use and accessibility debates are legal and product questions, not something a pipeline diagram settles.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;JioHotstar and similar services go black on capture because decrypted video is routed through a DRM-controlled path (EME/CDM on the web, MediaDrm/Widevine-class stacks on device) so the OS-level share or record surface never receives those pixels. Controls stay visible because they are ordinary UI, not the protected media plane.&lt;/p&gt;

&lt;p&gt;Understanding that split turns a confusing UX moment into a predictable security boundary: the invisible wall between licensed playback and uncontrolled redistribution.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/drm-screen-capture-jiohotstar/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 4 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>drm</category>
      <category>streaming</category>
      <category>webplatform</category>
      <category>security</category>
    </item>
    <item>
      <title>Browser Rendering Pipeline: How JS and CSS Become Pixels</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Wed, 08 Apr 2026 17:53:07 +0000</pubDate>
      <link>https://forem.com/helloashish99/browser-rendering-pipeline-how-js-and-css-become-pixels-55n7</link>
      <guid>https://forem.com/helloashish99/browser-rendering-pipeline-how-js-and-css-become-pixels-55n7</guid>
      <description>&lt;p&gt;Related: &lt;a href="https://renderlog.in/blog/16ms-frame-budget-60fps/" rel="noopener noreferrer"&gt;The 16.6ms Frame Budget&lt;/a&gt;, the wall clock deadline that every stage in this pipeline must fit inside.&lt;/p&gt;

&lt;p&gt;Every rendered frame runs through a fixed pipeline: &lt;strong&gt;Parse → Style → Layout → Paint → Composite&lt;/strong&gt;. Understanding which stage runs on which thread, what triggers each stage to re-run, and which stages can be skipped entirely is the mechanical foundation behind every browser performance optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this covers:&lt;/strong&gt; The full pipeline stage by stage, how the compositor thread separates from the main thread, what "jank" physically is at the hardware level, and how to read the DevTools flame chart to pinpoint which stage is your bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fifpdsgbwb7llnuetyxlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fifpdsgbwb7llnuetyxlp.png" alt="Diagram of the browser rendering pipeline stages from HTML parsing through compositing." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The pipeline, stage by stage
&lt;/h2&gt;

&lt;p&gt;When the browser receives HTML bytes, it doesn't hand them to a rendering function and wait. It runs a multi-stage pipeline, and each stage has a different cost profile and different triggers for re-running. Understanding the stages is the prerequisite for understanding &lt;em&gt;why&lt;/em&gt; your specific change makes something slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parse HTML → DOM tree
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;HTML parser&lt;/strong&gt; converts raw bytes into a tree of &lt;strong&gt;DOM nodes&lt;/strong&gt;. The parser is incremental; it doesn't wait for the full document. As it encounters &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags it may pause, execute the script synchronously (if not &lt;code&gt;async&lt;/code&gt; or &lt;code&gt;defer&lt;/code&gt;), then resume.&lt;/p&gt;

&lt;p&gt;The DOM is not the visual page. It's a tree of objects representing &lt;em&gt;content&lt;/em&gt; and &lt;em&gt;structure&lt;/em&gt;. Style is a separate concern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fetch CSS → CSSOM
&lt;/h3&gt;

&lt;p&gt;While the HTML parser runs, any &lt;code&gt;&amp;lt;link rel="stylesheet"&amp;gt;&lt;/code&gt; causes the browser to fetch the CSS. The &lt;strong&gt;CSSOM (CSS Object Model)&lt;/strong&gt; is built in parallel: a tree of rules, specificity-resolved, cascade-computed. The CSSOM &lt;strong&gt;blocks rendering&lt;/strong&gt;  the browser will not paint anything until it has enough CSS to avoid a flash of unstyled content.&lt;/p&gt;

&lt;p&gt;This is why render-blocking CSS matters for &lt;strong&gt;First Contentful Paint&lt;/strong&gt;: the larger and more complex your CSS, the later the browser can actually start putting pixels on screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merge → Render Tree
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;render tree&lt;/strong&gt; is a merger of the DOM and CSSOM. Crucially, it contains only &lt;em&gt;visible&lt;/em&gt; nodes. Elements with &lt;code&gt;display: none&lt;/code&gt; are absent. Elements with &lt;code&gt;visibility: hidden&lt;/code&gt; are present (they take up space). Pseudo-elements like &lt;code&gt;::before&lt;/code&gt; are included even though they don't exist in the DOM.&lt;/p&gt;

&lt;p&gt;This step is relatively cheap, but it happens whenever structural DOM or CSS changes occur.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layout (Reflow)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Layout&lt;/strong&gt; (also called &lt;strong&gt;reflow&lt;/strong&gt;) is where the browser figures out the &lt;em&gt;geometry&lt;/em&gt; of every element: position, width, height, margin, how text wraps. This is expensive because the layout of one element can cascade. Changing the width of a parent can reflow every child.&lt;/p&gt;

&lt;p&gt;Layout is &lt;strong&gt;the most painful stage&lt;/strong&gt; to trigger unnecessarily. It runs on the main thread and can take tens of milliseconds on a complex page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paint
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Paint&lt;/strong&gt; is where the browser fills in the actual pixels for each element's visual appearance: colors, borders, shadows, text. Paint produces &lt;strong&gt;display lists&lt;/strong&gt;  a set of drawing instructions  that are then handed to the GPU.&lt;/p&gt;

&lt;p&gt;Not all property changes trigger paint. Properties like &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; can be handled without repainting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composite
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Compositing&lt;/strong&gt; is where the browser takes individual &lt;strong&gt;layers&lt;/strong&gt; (more on those below) and combines them into the final image. This step happens on the &lt;strong&gt;compositor thread&lt;/strong&gt;, separate from the main thread. This is the key insight behind why &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; animations are "free": they only require compositing, not layout or paint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Main thread vs compositor thread vs GPU process
&lt;/h2&gt;

&lt;p&gt;Chrome has a multi-process architecture. The rendering work is split across at least three distinct actors.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thread/Process&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main thread&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript execution, style calculation, layout, paint display-list generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compositor thread&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Handles scroll, &lt;code&gt;transform&lt;/code&gt;/&lt;code&gt;opacity&lt;/code&gt; animations, layer compositing, independently of the main thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU process&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rasterizes layer display lists into actual pixels on the GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical implication: &lt;strong&gt;the compositor thread can keep animations and scrolling smooth even when the main thread is busy&lt;/strong&gt;. But only if those animations involve properties that don't require the main thread  &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt;. If your animation touches &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;padding&lt;/code&gt;, or &lt;code&gt;margin&lt;/code&gt;, it forces the main thread back into the loop.&lt;/p&gt;

&lt;p&gt;This is why you can see silky-smooth parallax scroll effects on a page that's simultaneously doing heavy JavaScript work, as long as the scroll transform is isolated to its own compositor layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3m4ll5fvjio07lv5s7v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3m4ll5fvjio07lv5s7v.png" alt="Diagram comparing work on the browser main thread versus the compositor thread and how they interact each frame." width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What "jank" physically is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Jank&lt;/strong&gt; is a frame drop. The display has a fixed refresh rate, commonly 60Hz, meaning it redraws 16.67ms. At each refresh, it either shows a new frame or repeats the previous one. If the browser misses the deadline, you see a repeated frame.&lt;/p&gt;

&lt;p&gt;This is called &lt;strong&gt;missing a v-sync&lt;/strong&gt;. The human visual system is highly tuned to smooth motion. A single dropped frame is barely perceptible. Two or three in a row is "stuttery." A consistent pattern of drops, caused by main-thread work exceeding 16ms, reads as a sluggish, broken UI even if the rest of the page is perfectly fine.&lt;/p&gt;

&lt;p&gt;The timeline looks like this in DevTools: frames that take longer than ~16ms appear highlighted in red in the frame timeline. The &lt;strong&gt;Performance&lt;/strong&gt; panel's frame section will show you each frame's actual duration; anything over 16ms is a potential jank source.&lt;/p&gt;




&lt;h2&gt;
  
  
  requestAnimationFrame and the pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt; (rAF) is the browser's invitation to do visual work &lt;em&gt;at the right moment&lt;/em&gt;. When you call &lt;code&gt;requestAnimationFrame(callback)&lt;/code&gt;, the browser schedules your callback to run &lt;strong&gt;at the start of the next frame&lt;/strong&gt;, just before layout and paint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateAnimations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// This runs before layout and paint, inside the frame budget&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translateX(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;calculatePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateAnimations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updateAnimations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What rAF does not do:&lt;/strong&gt; it doesn't guarantee your callback will fit inside 16ms. It just ensures you're called at a frame boundary instead of some arbitrary async point. If your rAF callback takes 30ms, you've still dropped a frame. The discipline of the 16ms budget is yours to maintain.&lt;/p&gt;

&lt;p&gt;Also important: rAF callbacks are batched to the display's refresh rate. If you call &lt;code&gt;requestAnimationFrame&lt;/code&gt; 500 times per second, you won't get 500 callbacks. You'll get approximately 60.&lt;/p&gt;




&lt;h2&gt;
  
  
  Forced reflow / layout thrashing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layout thrashing&lt;/strong&gt; is one of the most common and painful performance antipatterns. It happens when you &lt;strong&gt;write&lt;/strong&gt; to the DOM and then &lt;strong&gt;read&lt;/strong&gt; a layout-dependent property before the browser has had a chance to batch those updates.&lt;/p&gt;

&lt;p&gt;The browser is lazy about layout; it tries to defer it as long as possible. But certain property reads &lt;em&gt;force&lt;/em&gt; it to run layout immediately, because the value isn't valid until geometry is computed. These are called &lt;strong&gt;forced synchronous layouts&lt;/strong&gt; or &lt;strong&gt;forced reflows&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Properties that force layout when read include: &lt;code&gt;offsetWidth&lt;/code&gt;, &lt;code&gt;offsetHeight&lt;/code&gt;, &lt;code&gt;offsetTop&lt;/code&gt;, &lt;code&gt;offsetLeft&lt;/code&gt;, &lt;code&gt;clientWidth&lt;/code&gt;, &lt;code&gt;clientHeight&lt;/code&gt;, &lt;code&gt;scrollHeight&lt;/code&gt;, &lt;code&gt;scrollTop&lt;/code&gt;, &lt;code&gt;getBoundingClientRect()&lt;/code&gt;, &lt;code&gt;getComputedStyle()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's what the bad pattern looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layout thrashing  triggers layout on every iteration&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// FORCES layout to compute&lt;/span&gt;
  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Invalidates layout&lt;/span&gt;
  &lt;span class="c1"&gt;// Next iteration reads offsetHeight again  forces layout AGAIN&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to &lt;strong&gt;batch reads, then batch writes&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reads first: layout computed once&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Writes second: no interleaved forced layouts&lt;/span&gt;
&lt;span class="nx"&gt;heights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the bad version, if you have 100 elements, you're triggering 100 separate synchronous layouts. In the fixed version, you get one. This is the difference between a 2ms operation and a 200ms one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compositor layers: what promotes an element
&lt;/h2&gt;

&lt;p&gt;Not all elements are on the same &lt;strong&gt;compositor layer&lt;/strong&gt;. By default, most content lives on a single layer. But some properties cause the browser to &lt;strong&gt;promote an element to its own compositor layer&lt;/strong&gt;  meaning it gets its own GPU texture and can be transformed independently without touching the rest of the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Properties that promote to a new layer:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property/Condition&lt;/th&gt;
&lt;th&gt;Why it promotes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transform: translateZ(0)&lt;/code&gt; or &lt;code&gt;translate3d(0,0,0)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Forces GPU rasterization, historical "hack"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;will-change: transform&lt;/code&gt; or &lt;code&gt;will-change: opacity&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Explicit hint to browser to promote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;position: fixed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Must be composited independently from scroll&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Native GPU-accelerated content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS 3D transforms (&lt;code&gt;rotateX&lt;/code&gt;, &lt;code&gt;rotateY&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Requires 3D compositing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elements with &lt;code&gt;opacity &amp;lt; 1&lt;/code&gt; that also have children&lt;/td&gt;
&lt;td&gt;Blending requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why promotion matters:&lt;/strong&gt; once an element is on its own layer, you can animate its &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; without triggering layout or paint on the main thread. The compositor thread handles it entirely. This is how you get 60fps animations even on pages with heavy JS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; don't promote everything. Each layer is a GPU texture that consumes memory. Promoting hundreds of elements can exhaust GPU memory (especially on mobile) and cause the browser to swap textures in and out, which is &lt;em&gt;slower&lt;/em&gt; than not promoting in the first place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Good: targeted promotion for things you know you'll animate */&lt;/span&gt;
&lt;span class="nc"&gt;.animated-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;will-change&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Bad: promoting everything hoping for magic */&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* Please don't */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Style invalidation
&lt;/h2&gt;

&lt;p&gt;When you change a CSS class, the browser needs to figure out which elements are affected and recalculate their computed styles. This is &lt;strong&gt;style invalidation&lt;/strong&gt;, and it can be wider than you expect.&lt;/p&gt;

&lt;p&gt;If you add a class to the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, styles that use descendant selectors (&lt;code&gt;.theme-dark .card&lt;/code&gt;, &lt;code&gt;body.loading button&lt;/code&gt;) could match or unmatch for &lt;em&gt;any&lt;/em&gt; element in the tree. The browser must re-match selectors across the entire DOM.&lt;/p&gt;

&lt;p&gt;Specificity aside, there are practical rules that make style invalidation cheaper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Avoid deep descendant selectors&lt;/strong&gt; like &lt;code&gt;div &amp;gt; ul &amp;gt; li &amp;gt; span.text&lt;/code&gt;  the browser has to walk the tree to resolve them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use BEM or similar&lt;/strong&gt; so selectors are flat and high-specificity matches happen quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch class changes&lt;/strong&gt;  adding &lt;code&gt;classList.add('a', 'b', 'c')&lt;/code&gt; in one call is cheaper than three separate &lt;code&gt;.add()&lt;/code&gt; calls because it can trigger a single style recalculation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Performance panel's &lt;strong&gt;Style &amp;amp; Layout&lt;/strong&gt; section shows you "Recalculate Style" events. If you see them firing on every scroll or input event, that's style invalidation you can reduce.&lt;/p&gt;




&lt;h2&gt;
  
  
  Paint vs Composite: why &lt;code&gt;opacity&lt;/code&gt; and &lt;code&gt;transform&lt;/code&gt; are "free"
&lt;/h2&gt;

&lt;p&gt;This is the most important practical takeaway from the pipeline. The question "which CSS property is cheap to animate?" has a precise answer based on which pipeline stages the property affects.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Paint&lt;/th&gt;
&lt;th&gt;Composite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;padding&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;color&lt;/code&gt;, &lt;code&gt;background-color&lt;/code&gt;, &lt;code&gt;box-shadow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;opacity&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Properties in the third row skip layout and paint entirely. The compositor thread handles them without touching the main thread. This is why &lt;code&gt;transform: translateX()&lt;/code&gt; is categorically different from &lt;code&gt;left:&lt;/code&gt; when it comes to animation performance, even though they produce visually identical results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Slow: triggers layout and paint on every frame */&lt;/span&gt;
&lt;span class="nc"&gt;.bad-animation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;slide-left&lt;/span&gt; &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-left&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Fast: compositor-only, main thread not involved */&lt;/span&gt;
&lt;span class="nc"&gt;.good-animation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;slide-transform&lt;/span&gt; &lt;span class="m"&gt;300ms&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-transform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Reading the DevTools Performance flame chart
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Performance&lt;/strong&gt; panel in Chrome DevTools is the tool for diagnosing rendering bottlenecks. Here's how to read it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Record a trace:&lt;/strong&gt; open DevTools, go to Performance, hit Record, reproduce the sluggish interaction, stop recording.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frame timeline at top:&lt;/strong&gt; a series of bars representing each rendered frame. Green bars are on-time frames. Red-outlined bars are long frames (janky). Hover to see the exact duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The main thread flame chart:&lt;/strong&gt; this shows all work done on the main thread over time. The width of each block is how long it took. Blocks are stacked when one function calls another. You're looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wide yellow blocks&lt;/strong&gt;: JavaScript execution. Find long-running functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wide purple blocks&lt;/strong&gt;: Layout (Recalculate Style + Layout). Look for what triggered them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wide green blocks&lt;/strong&gt;: Paint events. Large paint areas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red triangles&lt;/strong&gt;: "Long task" markers, meaning the browser flagged a task over 50ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Clicking a Layout block&lt;/strong&gt; will show you in the bottom panel &lt;em&gt;which specific property read&lt;/em&gt; caused a forced synchronous layout. This is how I found that &lt;code&gt;offsetHeight&lt;/code&gt; call in the scroll listener  Chrome directly reported the call stack that triggered the forced layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Layers panel:&lt;/strong&gt; under the three-dot menu → More tools → Layers. Shows you a 3D view of your compositor layers. Useful for checking if you're accidentally promoting hundreds of elements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical triggers for each pipeline stage
&lt;/h2&gt;

&lt;p&gt;A quick reference for what causes the browser to run each stage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Common triggers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Style&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adding/removing CSS classes, inline style changes, pseudo-class changes (&lt;code&gt;:hover&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any geometry change (width, height, margin), reading forced-layout properties, font loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Paint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Color changes, shadow changes, visibility changes, border-radius, clip-path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Composite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;opacity&lt;/code&gt;, scroll position (on promoted elements)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you're debugging a performance problem, identifying which stage is the bottleneck tells you exactly where to look. A long "Recalculate Style" event points to selector complexity or broad invalidation. A long "Layout" event points to geometry-changing properties or forced reflows. A long "Paint" event points to large paint areas or expensive paint operations like &lt;code&gt;box-shadow&lt;/code&gt; and &lt;code&gt;filter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rendering pipeline is not a black box. Every frame of jank has a cause, and it's visible in the flame chart if you know where to look.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://renderlog.in/blog/browser-main-thread-rendering-pipeline/" rel="noopener noreferrer"&gt;renderlog.in&lt;/a&gt; · 12 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow &lt;a href="https://www.linkedin.com/in/ashish-cumar/" rel="noopener noreferrer"&gt;Ashish on LinkedIn&lt;/a&gt; for more frontend performance deep dives.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>browser</category>
      <category>rendering</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
