<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ashish Kumar</title>
    <description>The latest articles on DEV Community by Ashish Kumar (@helloashish99).</description>
    <link>https://dev.to/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>DEV Community: Ashish Kumar</title>
      <link>https://dev.to/helloashish99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/helloashish99"/>
    <language>en</language>
    <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://dev.to/helloashish99/drm-explained-why-jiohotstar-goes-black-when-you-screen-share-380</link>
      <guid>https://dev.to/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://dev.to/helloashish99/browser-rendering-pipeline-how-js-and-css-become-pixels-55n7</link>
      <guid>https://dev.to/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>
