Skip to main content

CSS Containment and Paint Worklets

Answer

CSS Containment and Paint Worklets are advanced CSS features for performance optimization and custom rendering. Containment limits the browser's rendering scope, while Paint Worklets allow custom drawing with JavaScript.

CSS Containment

CSS Containment (contain property) tells the browser that an element's content is independent from the rest of the DOM, allowing rendering optimizations.

Containment Values

.element {
/* Layout containment - element's layout is independent */
contain: layout;

/* Paint containment - content won't be drawn outside bounds */
contain: paint;

/* Size containment - element size doesn't depend on children */
contain: size;

/* Style containment - counters/quotes scoped to element */
contain: style;

/* Combine multiple */
contain: layout paint;

/* Shorthand for layout + paint + style */
contain: content;

/* Shorthand for all containment (strict) */
contain: strict;
}

Practical Examples

/* Virtualized list items */
.list-item {
contain: layout paint;
/* Browser knows item rendering is self-contained */
}

/* Article cards that won't affect page layout */
.card {
contain: content;
}

/* Fixed-size widget */
.widget {
contain: strict;
width: 300px;
height: 200px;
}

/* Infinite scroll container */
.scroll-container {
contain: layout;
overflow: auto;
}

content-visibility (Auto Containment)

/* Elements outside viewport are not rendered */
.article {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Placeholder size */
}

/* Always visible (default) */
.header {
content-visibility: visible;
}

/* Hidden but preserves layout */
.hidden-section {
content-visibility: hidden;
}

Performance Impact

/* Long page with many sections */
section {
content-visibility: auto;
contain-intrinsic-size: 0 800px;
}

/* Benefits:
* - Off-screen sections are not rendered
* - Faster initial paint
* - Scroll performance improved
* - Memory usage reduced
*/

CSS Paint Worklets (Houdini)

Paint Worklets allow custom drawing using Canvas-like APIs, creating backgrounds and borders impossible with regular CSS.

// my-paint-worklet.js
class CheckerboardPainter {
static get inputProperties() {
return ["--checkerboard-size", "--checkerboard-color"];
}

paint(ctx, size, properties) {
const squareSize = parseInt(properties.get("--checkerboard-size")) || 20;
const color = properties.get("--checkerboard-color") || "black";

ctx.fillStyle = color;

for (let y = 0; y < size.height / squareSize; y++) {
for (let x = 0; x < size.width / squareSize; x++) {
if ((x + y) % 2 === 0) {
ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
}
}
}
}
}

registerPaint("checkerboard", CheckerboardPainter);
/* Register and use the worklet */
.checkerboard-bg {
--checkerboard-size: 30;
--checkerboard-color: rgba(0, 0, 0, 0.1);
background-image: paint(checkerboard);
}
// Register in your main JS
CSS.paintWorklet.addModule("my-paint-worklet.js");

Paint Worklet Use Cases

// Gradient noise texture
class NoisePainter {
paint(ctx, size) {
const imageData = ctx.createImageData(size.width, size.height);
for (let i = 0; i < imageData.data.length; i += 4) {
const value = Math.random() * 255;
imageData.data[i] = value;
imageData.data[i + 1] = value;
imageData.data[i + 2] = value;
imageData.data[i + 3] = 20; // Low opacity
}
ctx.putImageData(imageData, 0, 0);
}
}
registerPaint("noise", NoisePainter);
.noisy-background {
background-image: paint(noise);
}

/* Combine with regular backgrounds */
.fancy-bg {
background: paint(noise), linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

Browser Support & Fallbacks

/* Check for Houdini support */
@supports (background: paint(something)) {
.element {
background-image: paint(custom);
}
}

/* Fallback for older browsers */
.element {
background-color: #667eea; /* Fallback */
background-image: paint(gradient); /* Modern */
}

will-change Optimization

/* Hint to browser about upcoming changes */
.animating-element {
will-change: transform, opacity;
/* Browser can prepare optimizations */
}

/* ❌ Don't overuse */
* {
will-change: transform; /* Performance hit! */
}

/* ✅ Apply only when needed */
.card:hover {
will-change: transform;
}

.card.animating {
will-change: transform;
}

/* Remove after animation */
.card {
will-change: auto;
}

Performance Best Practices

/* Combine containment for best results */
.optimized-component {
contain: content;
content-visibility: auto;
contain-intrinsic-size: 200px 300px;
will-change: transform; /* Only if animating */
}

/* Off-screen images */
.lazy-section {
content-visibility: auto;
}

/* Large lists */
.virtual-list-item {
contain: layout style paint;
}

Key Points

  • contain limits rendering scope for performance
  • content-visibility: auto skips rendering off-screen content
  • Paint Worklets enable custom CSS backgrounds via JS
  • Use will-change sparingly for animations
  • Containment is most effective on isolated components
  • Test performance improvements with DevTools