From harness-claude
Explains browser paint and compositing pipeline to achieve 60fps animations, identify repaint triggers, use compositor-only CSS properties, and manage GPU layers without memory exhaustion. Use for poor animation performance or high layer counts in DevTools.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Understand the browser's paint and compositing pipeline — how content is rasterized into layers, which properties trigger expensive repaints, how GPU compositing enables 60fps animations, and how to manage layer promotion without exhausting GPU memory.
Audits and fixes animation performance issues including layout thrashing, compositor properties, scroll-linked motion, and blur effects. Use for stuttering animations, janky transitions, or CSS/JS animation reviews.
Audits and fixes UI animation performance issues including layout thrashing, compositor properties, scroll-linked motion, and blur effects. Use when animations stutter, transitions jank, or reviewing CSS/JS animation performance.
Diagnoses and fixes slow or janky web animations using Disney's 12 principles. Use for frame drops below 60fps, stuttering, UI sluggishness, or layout thrashing with CSS/JS optimizations.
Share bugs, ideas, or general feedback.
Understand the browser's paint and compositing pipeline — how content is rasterized into layers, which properties trigger expensive repaints, how GPU compositing enables 60fps animations, and how to manage layer promotion without exhausting GPU memory.
left/top versus transform: translate()will-change is applied to many elements and you need to understand the memory trade-offUnderstand the two-thread model. The browser has a main thread (runs JavaScript, style, layout, paint records) and a compositor thread (composites layers, handles scroll, runs transform/opacity animations). Properties that only affect compositing (transform, opacity) animate on the compositor thread without blocking JavaScript execution.
Identify compositor-only properties. Only these CSS properties can be animated without triggering layout or paint:
transform — translate, scale, rotate, skew (GPU-composited)opacity — alpha blending on the GPUfilter — GPU-accelerated in most browsers (but check paint in DevTools)backdrop-filter — composited but expensive on mobile/* EXPENSIVE — triggers Layout + Paint + Composite every frame */
.slide {
transition:
left 0.3s,
top 0.3s;
}
/* CHEAP — Composite only, runs on compositor thread */
.slide {
transition: transform 0.3s;
}
Promote elements to their own layer deliberately. Layer promotion moves an element to a separate GPU texture, allowing the compositor to animate it independently. Promote with:
/* Promote only when animation is about to start */
.card.will-animate {
will-change: transform;
}
/* Alternative: 3D transform hack (older technique) */
.promoted {
transform: translateZ(0);
}
Remove will-change after animations complete. Each promoted layer consumes GPU memory (the element is rasterized to a separate texture). Remove the hint when not needed:
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});
Use the Layers panel to audit composited layers. In Chrome DevTools, open the Layers panel (More tools > Layers). Each green-outlined rectangle is a composited layer. Check:
Reduce paint complexity. Some CSS properties are expensive to paint:
box-shadow — especially with large blur radius (>10px), painted per frame during animationborder-radius with overflow: hidden — requires clipping maskfilter: blur() — full-surface Gaussian blurbackground: linear-gradient() — repainted on size changesPrefer pre-rendered images or SVGs for complex visual effects that change frequently.
After layout, the browser creates a "paint record" — an ordered list of drawing commands (draw rectangle, draw text, draw image). This record is then rasterized into pixels. Modern browsers use two rasterization strategies:
Rasterization is tiled: the page is divided into 256x256px tiles, and only visible tiles are rasterized. During scroll, new tiles are rasterized on background threads (off-main-thread rasterization).
Airbnb's listing page had a parallax hero image that animated at 15fps on mobile. The implementation used background-position to create the parallax effect, which triggers paint on every scroll frame because the browser must re-rasterize the background at a new position.
/* BEFORE: 15fps — triggers paint on every scroll frame */
.hero {
background-position: center calc(50% + var(--scroll-offset));
}
/* AFTER: 60fps — compositor-only, no paint */
.hero-image {
will-change: transform;
transform: translate3d(0, var(--scroll-offset), 0);
}
The fix moved from background-position (paint per frame) to transform: translate3d() (compositor-only). Frame rate went from 15fps to 60fps because the compositor thread handles the transform without involving the main thread.
A real-time dashboard applied will-change: transform to all 50 chart widgets for smooth updates. Each widget was 400x300px at 2x device pixel ratio, creating 50 layers at 4003004*4 = 1.92MB each (RGBA, 2x DPR) = 96MB of GPU memory just for chart layers. On mobile devices with 256MB GPU memory budget, this caused texture eviction and janky re-rasterization.
Fix: apply will-change only to the chart currently being updated, remove it after the update animation completes. GPU memory dropped from 96MB to ~4MB (2 active layers at any time).
Elements are promoted to their own composited layer when:
will-change: transform, will-change: opacity, or will-change: filter is settransform: translate3d(), transform: translateZ())<video>, <canvas>, or <iframe> elements (always composited)position: fixed or position: sticky elementstransform or opacityImplicit promotion (overlap-based) is a common source of unexpected layer count increases. Use the Layers panel to identify "compositing reason: overlaps other composited content."
Blanket will-change: transform on all elements. Each promoted layer is rasterized to a separate GPU texture. Applying will-change to 50+ elements on a page with no active animations wastes GPU memory and can cause worse performance than no promotion at all due to texture management overhead.
Animating box-shadow directly. box-shadow triggers paint on every frame. For animated shadows, use a pseudo-element with the shadow pre-applied and animate its opacity:
.card {
position: relative;
}
.card::after {
content: '';
position: absolute;
inset: 0;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.3s;
}
.card:hover::after {
opacity: 1;
}
Excessive layer count on mobile. Mobile GPUs have limited texture memory (128-512MB shared across all apps). More than 30 composited layers on mobile risks texture eviction, where the GPU must re-upload textures from main memory, causing visible jank during scroll or animation.
backface-visibility: hidden hack applied globally. This was a common trick to force GPU compositing, but applying it to all elements creates unnecessary layers, wastes GPU memory, and can cause text rendering differences (subpixel antialiasing is disabled on composited layers in some browsers).
Animating border-radius or clip-path directly. These trigger paint recalculation per frame. Instead, pre-create the clipped shape and animate transform or opacity on the container.