{"id":"l-css-4-5","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"Know which CSS changes trigger layout, paint or only compositing, animate the cheap properties, and use content-visibility and aspect-ratio to keep long pages fast and stable.","html":"<p>Waypoint’s schedule looked finished, and then the team opened it on a three-year-old Android phone at the venue’s busy Wi-Fi. The save toast stuttered, the first scroll felt sticky, and speaker photos pushed the text down as they loaded. Nothing was broken; it was just doing more work than it needed to.</p>\n<p>CSS performance comes down to one question: <em>how much of the rendering pipeline does this change force the browser to run?</em></p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The rendering pipeline</h2>\n<p>After your CSS changes something, the browser runs up to four stages to update the screen:</p>\n<ol>\n<li><strong>Style</strong>: work out which rules apply and compute each element’s values.</li>\n<li><strong>Layout</strong>: calculate every box’s size and position.</li>\n<li><strong>Paint</strong>: fill in the pixels: text, colors, borders, shadows, images.</li>\n<li><strong>Composite</strong>: assemble the painted layers on the GPU, applying transforms and opacity.</li>\n</ol>\n<p>The trick is that each change only needs to start at a certain stage. Change <code dir=\"ltr\">width</code> and layout must run, then paint, then compositing. Change <code dir=\"ltr\">background-color</code> and layout can be skipped. Change <code dir=\"ltr\">transform</code> or <code dir=\"ltr\">opacity</code> on an element that has its own layer, and the browser can skip layout <em>and</em> paint, moving existing pixels around on the GPU.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-css-4-5-t1\"><defs><marker id=\"arrow-l-css-4-5-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-css-4-5-t1\">Four pipeline stages left to right: style, layout, paint, composite. A row for width, top and margin runs through all four. A row for color, background and box-shadow skips layout. A row for transform and opacity skips layout and paint.</title>\n  <rect class=\"d-box\" x=\"170\" y=\"16\" width=\"110\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"196\" y=\"40\">Style</text>\n  <rect class=\"d-box\" x=\"295\" y=\"16\" width=\"110\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"319\" y=\"40\">Layout</text>\n  <rect class=\"d-box\" x=\"420\" y=\"16\" width=\"110\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"451\" y=\"40\">Paint</text>\n  <rect class=\"d-box\" x=\"545\" y=\"16\" width=\"120\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"557\" y=\"40\">Composite</text>\n  <text class=\"d-code\" x=\"10\" y=\"94\">width, top</text>\n  <rect class=\"d-box-warn\" x=\"170\" y=\"76\" width=\"495\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"180\" y=\"95\">all four stages, every frame</text>\n  <text class=\"d-code\" x=\"10\" y=\"144\">box-shadow</text>\n  <rect class=\"d-box-accent\" x=\"170\" y=\"126\" width=\"110\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-accent\" x=\"420\" y=\"126\" width=\"245\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"303\" y=\"145\">skipped</text>\n  <text class=\"d-code\" x=\"10\" y=\"194\">translate</text>\n  <rect class=\"d-box-success\" x=\"170\" y=\"176\" width=\"110\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-success\" x=\"545\" y=\"176\" width=\"120\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"370\" y=\"195\">skipped</text>\n</svg><figcaption>Which stages a property change triggers</figcaption></figure>\n<p>For a single click, the difference doesn’t matter. For an animation running 60 times a second, or a style change that touches 500 elements, it decides whether the page feels smooth.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Animate the cheap properties</h2>\n<p>The save toast was animated like this:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> toast-in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">left</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">-320</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  to</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">left</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">left</code> is a geometry property: every frame re-runs layout for the toast (and checks whether anything else moved), then repaints. The fix keeps the toast’s position fixed and moves its picture instead:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  position</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">fixed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  left</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  bottom</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: toast-in </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> toast-in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">-336</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  to</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;      </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Same visual result, a fraction of the work. This is why every animation in this course used <code dir=\"ltr\">translate</code>, <code dir=\"ltr\">scale</code>, <code dir=\"ltr\">opacity</code> and friends. The same reasoning applies to transitions: a hover that changes <code dir=\"ltr\">box-shadow</code> repaints on each frame, which is fine for one card and noticeable for a grid of fifty animating at once. Fading in a pre-painted shadow on a pseudo-element with <code dir=\"ltr\">opacity</code> is the cheaper alternative when it matters.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>will-change everywhere</span></p>\n<p><code dir=\"ltr\">will-change: transform</code> tells the browser to promote an element to its own layer in advance. On the one element about to animate, that can avoid a hitch at the start. On every card “just in case”, it creates dozens of layers that each cost GPU memory, which can make the page slower, especially on phones. Add it right before an animation (or on <code dir=\"ltr\">:hover</code> of the parent) and let it go afterwards; usually you don’t need it at all.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Skip work you can’t see</h2>\n<p>The schedule renders two days of sessions, but the user sees a screenful at a time. <code dir=\"ltr\">content-visibility: auto</code> lets the browser skip layout and paint for sections that are off-screen:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.day</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  content-visibility</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  contain-intrinsic-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Skipped content has no size, so <code dir=\"ltr\">contain-intrinsic-size</code> gives the browser an estimate to use as a placeholder; with <code dir=\"ltr\">auto</code>, it remembers the real size once the section has been rendered. Without it, sections collapse to zero and the scrollbar jumps as you scroll. Content stays in the DOM and in the accessibility tree, and find-in-page still works. <code dir=\"ltr\">content-visibility: auto</code> has been Baseline since September 2025, when Safari 26 added the <code dir=\"ltr\">auto</code> value; a browser without it simply renders every section, as before.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Don’t move things after they appear</h2>\n<p>Cumulative Layout Shift, one of the Core Web Vitals, measures content jumping as the page loads. In CSS, the usual culprit is media without reserved space. A speaker photo with <code dir=\"ltr\">width: 100%</code> and no height is 0px tall until the image arrives, then shoves everything down. Reserve the space:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.speaker</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> img</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">100</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  height</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  aspect-ratio</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  object-fit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">cover</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">width</code> and <code dir=\"ltr\">height</code> attributes on the <code dir=\"ltr\">&lt;img&gt;</code> do the same job, since browsers derive an aspect ratio from them. For web fonts, <code dir=\"ltr\">font-display: swap</code> plus a fallback font with similar metrics keeps text from reflowing when the font loads.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Measure first</span></p>\n<p>Open the DevTools Performance panel, record a scroll or an interaction, and look for long purple (style and layout) and green (paint) blocks. The Rendering panel’s “Paint flashing” option highlights what repaints. Optimise what the trace shows, not what you suspect.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Where you are now</h2>\n<p>Across twenty lessons you’ve taken Waypoint’s schedule from a stylesheet that fought you to one you can predict: layers and <code dir=\"ltr\">:where()</code> settle conflicts, custom properties expose component APIs, grid, subgrid and container queries handle layout, <code dir=\"ltr\">oklch()</code> and <code dir=\"ltr\">light-dark()</code> handle color, logical properties make it work in Arabic, and motion is kind and cheap. The habit to keep is the one from the first lesson: before you change CSS, predict what the browser will do, then check.</p>\n","toc":[{"id":"sec-1","text":"The rendering pipeline","level":2},{"id":"sec-2","text":"Animate the cheap properties","level":2},{"id":"sec-3","text":"Skip work you can’t see","level":2},{"id":"sec-4","text":"Don’t move things after they appear","level":2},{"id":"sec-5","text":"Where you are now","level":2}],"takeaways":["Every visual change goes through some of style, layout, paint and composite; geometry properties trigger layout, visual properties trigger paint, and <code dir=\"ltr\">transform</code> and <code dir=\"ltr\">opacity</code> can skip both.","Animate <code dir=\"ltr\">translate</code>, <code dir=\"ltr\">scale</code>, <code dir=\"ltr\">rotate</code> and <code dir=\"ltr\">opacity</code>; animating <code dir=\"ltr\">left</code>, <code dir=\"ltr\">width</code> or <code dir=\"ltr\">margin</code> re-runs layout on every frame.","<code dir=\"ltr\">content-visibility: auto</code> with <code dir=\"ltr\">contain-intrinsic-size</code> lets the browser skip rendering off-screen sections of a long page.","Reserving space with <code dir=\"ltr\">aspect-ratio</code> (or width and height attributes) prevents layout shifts when images and embeds load.","Measure before optimising, with the DevTools Performance panel and paint flashing, and remove <code dir=\"ltr\">will-change</code> once an animation ends."],"further":[{"title":"Rendering performance (web.dev)","url":"https://web.dev/articles/rendering-performance"},{"title":"content-visibility (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/content-visibility"},{"title":"Optimize Cumulative Layout Shift (web.dev)","url":"https://web.dev/articles/optimize-cls"}],"quiz":[{"q":"<p>A toast slides in by animating <code dir=\"ltr\">left</code> from <code dir=\"ltr\">-320px</code> to <code dir=\"ltr\">16px</code>. On a mid-range phone it stutters. What’s the best fix?</p>","options":[{"text":"Add <code dir=\"ltr\">will-change: left</code> to the toast.","why":"<code dir=\"ltr\">will-change</code> can’t make <code dir=\"ltr\">left</code> cheap; changing it still re-runs layout every frame."},{"text":"Animate <code dir=\"ltr\">translate</code> from <code dir=\"ltr\">-336px 0</code> to <code dir=\"ltr\">0 0</code> instead, keeping <code dir=\"ltr\">left</code> fixed.","why":"Correct. <code dir=\"ltr\">translate</code> changes only how the already-painted toast is composited, so the browser can skip layout and paint during the animation."},{"text":"Shorten the animation to 50ms so there are fewer frames.","why":"The stutter comes from per-frame layout work; a 50ms slide is also too fast for people to follow."},{"text":"Use a JavaScript animation library instead of CSS.","why":"A JS library animating <code dir=\"ltr\">left</code> hits the same layout cost, and runs on the busy main thread too."}],"answer":1},{"q":"<p>Which property change usually causes a repaint but no layout?</p>","options":[{"text":"<code dir=\"ltr\">padding</code>","why":"Padding changes an element’s box size, so layout runs (and then paint)."},{"text":"<code dir=\"ltr\">font-size</code>","why":"Font size changes text metrics and line breaks, which is a layout change."},{"text":"<code dir=\"ltr\">background-color</code>","why":"Correct. The geometry doesn’t change, so layout is skipped, but the pixels must be repainted."}],"answer":2},{"q":"<p>The schedule has 60 session cards across two days, and the initial render is slow. You add <code dir=\"ltr\">content-visibility: auto</code> to each day section. What else should you add?</p>","options":[{"text":"<code dir=\"ltr\">contain-intrinsic-size: auto 1200px</code>, so skipped sections keep an estimated height and the scrollbar doesn’t jump.","why":"Correct. Skipped content has no size of its own; the intrinsic size placeholder (remembered once rendered, thanks to <code dir=\"ltr\">auto</code>) keeps scrolling stable."},{"text":"<code dir=\"ltr\">will-change: contents</code> on every card.","why":"That asks the browser to prepare every card for change, which costs memory and undoes the saving."},{"text":"Nothing; content-visibility handles sizing automatically.","why":"Without a size estimate, skipped sections collapse to zero height, so the page and scrollbar jump as you scroll."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<section class=\"day\">\n  <h2>Day 1</h2>\n  <article class=\"speaker\">\n    <div class=\"photo\" role=\"img\" aria-label=\"Ana Ortiz\"></div>\n    <p>Ana Ortiz</p>\n  </article>\n</section>\n<section class=\"day\">\n  <h2>Day 2</h2>\n  <p>Twenty-eight sessions…</p>\n</section>\n<div class=\"toast\" role=\"status\">Saved to your schedule</div>","css":".speaker { width: 160px; }\n.photo {\n  width: 100%;\n  background: #ede9fe;\n  border-radius: 12px;\n}\n.toast {\n  position: fixed;\n  left: 16px;\n  bottom: 16px;\n  padding: 8px 14px;\n  border-radius: 8px;\n  background: #1f2937;\n  color: white;\n  animation: toast-in 250ms ease-out;\n}\n\n@keyframes toast-in {\n  from { left: -320px; }\n  to { left: 16px; }\n}\n"},"solution":{"html":"<section class=\"day\">\n  <h2>Day 1</h2>\n  <article class=\"speaker\">\n    <div class=\"photo\" role=\"img\" aria-label=\"Ana Ortiz\"></div>\n    <p>Ana Ortiz</p>\n  </article>\n</section>\n<section class=\"day\">\n  <h2>Day 2</h2>\n  <p>Twenty-eight sessions…</p>\n</section>\n<div class=\"toast\" role=\"status\">Saved to your schedule</div>","css":".speaker { width: 160px; }\n.photo {\n  width: 100%;\n  background: #ede9fe;\n  border-radius: 12px;\n}\n.toast {\n  position: fixed;\n  left: 16px;\n  bottom: 16px;\n  padding: 8px 14px;\n  border-radius: 8px;\n  background: #1f2937;\n  color: white;\n  animation: toast-in 250ms ease-out;\n}\n.photo { aspect-ratio: 1; }\n\n.day {\n  content-visibility: auto;\n  contain-intrinsic-size: auto 1200px;\n}\n\n@keyframes toast-in {\n  from { translate: -336px 0; opacity: 0; }\n  to { translate: 0 0; opacity: 1; }\n}\n"},"checks":[{"id":"toast-keyframes-cheap","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); const kf = all.find((r) => r instanceof CSSKeyframesRule && r.name === 'toast-in'); if (!kf) return false; const props = [...kf.cssRules].flatMap((f) => [...f.style]); const cheap = ['translate', 'transform', 'opacity', 'scale']; return props.length > 0 && props.every((p) => cheap.includes(p)) && props.some((p) => p === 'translate' || p === 'transform');"},{"id":"toast-still-animates","test":"return style('.toast', 'animation-name') === 'toast-in' && parseFloat(style('.toast', 'animation-duration')) > 0;"},{"id":"toast-final-position","test":"await tick(400); const r = $('.toast').getBoundingClientRect(); return Math.abs(r.left - 16) < 1;"},{"id":"skip-offscreen","test":"return $$('.day').every((d) => style(d, 'content-visibility') === 'auto');"},{"id":"intrinsic-size","test":"return $$('.day').every((d) => { const v = style(d, 'contain-intrinsic-size'); return v.includes('auto') && /\\d/.test(v); });"},{"id":"photo-reserves-space","test":"const r = $('.photo').getBoundingClientRect(); return r.width > 0 && Math.abs(r.height - r.width) < 1;"}],"title":"Make the schedule cheap to render","prompt":"<p>Three performance fixes, all in CSS:</p>\n<ol>\n<li>Rewrite the <code dir=\"ltr\">toast-in</code> keyframes so the toast slides in from the left using only <strong>compositor-friendly</strong> properties (<code dir=\"ltr\">translate</code> or <code dir=\"ltr\">transform</code>, plus <code dir=\"ltr\">opacity</code> if you like). Keep the name <code dir=\"ltr\">toast-in</code>, and make sure the toast ends at <code dir=\"ltr\">left: 16px</code>.</li>\n<li>Let the browser skip rendering off-screen <code dir=\"ltr\">.day</code> sections, with an <strong>intrinsic size estimate</strong> that uses <code dir=\"ltr\">auto</code> and a length.</li>\n<li>Make the <code dir=\"ltr\">.photo</code> frame <strong>square</strong> before any image loads, so it reserves its space.</li>\n</ol>\n","hints":["The toast’s <code dir=\"ltr\">left: 16px</code> can stay as is; animate how far it’s translated from that spot instead.","<code dir=\"ltr\">content-visibility: auto</code> pairs with <code dir=\"ltr\">contain-intrinsic-size: auto &lt;length&gt;</code>.","<code dir=\"ltr\">@keyframes toast-in { from { translate: -336px 0; opacity: 0; } to { translate: 0 0; opacity: 1; } }</code>, <code dir=\"ltr\">.photo { aspect-ratio: 1; }</code>."],"explanation":"<p>Animating <code dir=\"ltr\">left</code> changes geometry, so every frame re-ran layout and paint. <code dir=\"ltr\">translate</code> and <code dir=\"ltr\">opacity</code> only change how the toast’s already-painted layer is composited, so the browser can run the animation without layout or paint, often off the main thread. <code dir=\"ltr\">content-visibility: auto</code> lets the browser skip off-screen day sections entirely, and <code dir=\"ltr\">contain-intrinsic-size: auto 1200px</code> stands in for their height until they’ve been rendered once, so the scrollbar stays stable. <code dir=\"ltr\">aspect-ratio: 1</code> gives the photo frame a height from its width, so nothing shifts when the image arrives.</p>\n","checkLabels":{"toast-keyframes-cheap":"toast-in animates only translate/transform and opacity","toast-still-animates":"The toast still runs the toast-in animation","toast-final-position":"The toast ends at its normal position","skip-offscreen":"Day sections use content-visibility: auto","intrinsic-size":"Day sections have an intrinsic size estimate with auto","photo-reserves-space":"The photo frame is square before the image loads"}},"words":766,"readMinutes":4}