# Paint & Composite Optimization > Animate only transform and opacity to skip paint entirely. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/paint-composite-optimization Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## Summary The browser rendering pipeline has four stages: style, layout, paint, and composite. Animating properties that trigger layout or paint on every frame destroys frame rate. Restricting motion to `transform` and `opacity` lets the compositor thread run animations off the main thread at 60 fps, even while JavaScript is busy. ## Compositor thread The browser splits rendering into a main thread (style, layout, paint) and a compositor thread (compositing layers into the final frame). The compositor thread can animate `transform` and `opacity` without touching the main thread at all — which means those animations run at 60 fps even during a JavaScript-heavy task. Any other animated property (color, width, top, left) forces the main thread to repaint before the compositor can proceed. ## Diagram ```mermaid flowchart LR A[Style] --> B[Layout] B --> C[Paint] C --> D[Composite] E[transform / opacity] -->|skips to| D F[top / left / width] -->|triggers| B ``` Animating `transform`/`opacity` jumps straight to the composite stage; geometry and color properties restart the pipeline from layout or paint. ## transform vs top/left — and content-visibility Two patterns side by side: replacing a layout-triggering animation with a compositor-only one, then using `content-visibility: auto` to skip paint for off-screen sections entirely. **Compositor-safe animation + content-visibility** ```css /* ❌ triggers layout on every frame */ .card-bad { transition: top 300ms ease, left 300ms ease; } /* ✓ compositor-only: no layout, no paint */ .card-good { transition: transform 300ms ease; } /* Promote an element to its own layer when you know it will animate */ .drawer { will-change: transform; /* The browser allocates a GPU texture for this element now. Remove will-change once the animation ends to free the memory. */ } /* Skip painting off-screen sections entirely */ .article-section { content-visibility: auto; contain-intrinsic-size: auto 300px; /* estimated height to hold scroll position */ } /* CSS containment — tell the browser a subtree is independent */ .widget { contain: layout style paint; /* layout: children cannot affect outside geometry style: counters/quotes don't escape paint: contents are clipped and not painted outside the box */ } ``` `transform` and `opacity` run on the compositor thread. `content-visibility: auto` skips rendering off-screen sections, cutting the rendering cost of off-screen content substantially on long pages. `contain` narrows the scope of style/layout recalculations. ## Rules for jank-free animation - Animate only `transform` and `opacity` — both are compositor-only properties. - `will-change: transform` promotes an element to its own GPU layer before animation starts. - `will-change` costs GPU memory; apply it just before animation and remove it after. - `content-visibility: auto` skips layout and paint for off-screen content sections. - `contain: layout style paint` isolates a subtree so changes inside it don't retrigger outside. - A "paint storm" occurs when a single state change invalidates paint for the whole page; containment limits the blast radius. ## Watch out: will-change is not free Every element with `will-change: transform` gets its own GPU texture — even when idle. Slapping it on dozens of cards inflates GPU memory and can cause jank on low-end devices. Apply it dynamically (add the class on `mouseenter`, remove on `animationend`) rather than statically in a stylesheet. ## Key terms - **compositor thread**: Browser thread that combines GPU layers into the final frame, independent of the main thread. - **will-change**: CSS hint that promotes an element to its own GPU layer before animation, at a memory cost. - **content-visibility: auto**: Skips layout and paint for off-screen elements; browser re-renders them when they scroll into view. - **CSS contain**: Property that limits how much a subtree's changes affect the rest of the document's layout and paint. - **paint storm**: A single DOM or style change that invalidates and repaints a large, uncontained region of the page. ## Related topics - [The Rendering Pipeline](https://fearchitect.com/topics/rendering-pipeline.md): Style → Layout → Paint → Composite: what triggers each stage. - [Event Loop & scheduler.yield()](https://fearchitect.com/topics/event-loop-scheduler-yield.md): Break long tasks to keep the main thread responsive and hit INP. - [Render Performance](https://fearchitect.com/topics/render-performance-patterns.md): Skip renders, defer slow work, and virtualize long lists. - [Core Web Vitals](https://fearchitect.com/topics/core-web-vitals.md): Google's three user-experience metrics: LCP, INP, and CLS. - [View Transitions & Scroll Animations](https://fearchitect.com/topics/view-transitions-scroll-animations.md): CSS-native page transitions and scroll-driven animations without JavaScript. - [Web Workers & Off-Main-Thread](https://fearchitect.com/topics/web-workers-off-main-thread.md): Offload CPU-heavy work to a background thread, keeping the main thread free. ## Further reading - [MDN — will-change](https://developer.mozilla.org/en-US/docs/Web/CSS/will-change) - [MDN — content-visibility](https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility) - [MDN — contain](https://developer.mozilla.org/en-US/docs/Web/CSS/contain) - [web.dev — content-visibility: the property that boosts rendering performance](https://web.dev/articles/content-visibility) - [CSS Triggers (archived reference for paint/layout/composite property costs)](https://csstriggers.com/)