| |

Tailwind CSS 78 🎨 Performance Profiling for CSS Calculation, Repaints, and Layout Shifts

⏱️ Estimated read: 13 min | |

The browser renders a page in a pipeline: the style calculation, the layout, the paint, and the composite. Each stage costs time, and each stage can be the bottleneck. A CSS change that looks harmless in the source can trigger the expensive recalculation, the reflow of the whole page, or the repaint of the large area. The Tailwind utilities are the specific declarations, and the specific declarations have the specific performance characteristics. This chapter covers the pipeline, the profiling tools, and the patterns that keep the CSS off the critical path.

Key point: The browser runs the four stages on the main thread: the style, the layout, the paint, and the composite. The transform and the opacity are the only two properties the browser can animate on the compositor thread without the main-thread work. Every other property change triggers the style, the layout, or the paint.


Why the profiling matters

The main-thread problem. The main thread runs the JavaScript, the style calculation, the layout, and the paint. The long task blocks the interaction, and the user perceives the jank. The CSS that triggers the layout on every frame is the main-thread cost.

The layout problem. The property that changes the geometry (the width, the height, the top, the margin, the padding, the font-size) triggers the layout. The layout is the expensive, and the scope is the subtree or the whole document.

The paint problem. The property that changes the visual but not the geometry (the background-color, the color, the box-shadow, the border-radius) triggers the paint. The paint is the cheaper than the layout, and the scope is the layer.

The composite problem. The property the browser can handle on the compositor thread (the transform, the opacity, the filter) skips the main thread. The composite is the cheapest, and the scope is the layer.

The CLS problem. The Cumulative Layout Shift is the metric that measures the unexpected layout movement. The image without the dimensions, the font swap, and the ad insertion are the common causes. The Tailwind’s aspect-ratio utilities are the fix for the image.

The Tailwind problem. The utilities are the small declarations, and the small declarations are the fast. The p-4 is the one declaration, and the transition-all is the one property that triggers the everything. The audit of the utilities is the audit of the pipeline.

The measurement problem. The Chrome DevTools Performance panel records the trace, and the flame chart shows the stages. The Layout Shift and the Paint Flashing are the tools, and the PerformanceObserver is the API.


a. The render pipeline

The browser has the four stages. Each stage has the cost, and the CSS property determines the stage.

The style calculation

The style calculation matches the selectors, the resolves the cascade, and the computes the final styles. The cost is proportional to the number of the elements and the complexity of the selectors. The .a .b .c selector is the more expensive than the .a selector.

/* The simple selector */
.card { padding: 16px; }

/* The complex selector */
.page .content .card .title { font-size: 24px; }

The Tailwind utilities are the single-class, and the single-class is the fast.

The layout

The layout computes the geometry. The property that changes the position or the size of an element triggers the layout. The width, the height, the margin, the padding, the border, the font-size, and the display are the layout-triggering.

// The layout-triggering change
<div className="w-64">...</div>
// The width change triggers the layout.

The layout’s cost is the O(n) in the number of the affected elements.

The paint

The paint fills the pixels. The property that changes the visual without the geometry triggers the paint. The color, the background-color, the border-color, the box-shadow, and the border-radius are the paint-triggering.

// The paint-triggering change
<div className="bg-blue-600">...</div>
// The background-color change triggers the paint.

The paint’s cost is the O(area) of the affected region.

The composite

The composite merges the layers. The transform, the opacity, and the filter are the compositor-friendly. The browser can handle them on the compositor thread, and the main thread is free.

// The compositor-friendly change
<div className="transition-transform hover:scale-110">...</div>
// The transform change is the compositor-only.

The composite’s cost is the O(layers).

The pipeline table

The stageThe propertiesThe cost
The styleThe selectors, the cascadeThe O(elements)
The layoutThe geometryThe O(affected)
The paintThe visualThe O(area)
The compositeThe transform, the opacityThe O(layers)

The CSS property table

The propertyThe stage
width, heightThe layout
margin, paddingThe layout
font-size, line-heightThe layout
background-colorThe paint
color, border-colorThe paint
box-shadow, border-radiusThe paint
transformThe composite
opacityThe composite
filterThe composite

b. The profiling tools

The Chrome DevTools and the PerformanceObserver are the tools.

The Performance panel

The panel records the trace, and the flame chart shows the stages. The Layout, the Recalculate Style, the Paint, and the Composite Layers are the entries.

The trace:
  - The task: 45ms
    - The Recalculate Style: 12ms
    - The Layout: 20ms
    - The Paint: 8ms
    - The Composite Layers: 5ms

The long task is the bottleneck.

The Layout Shift regions

The DevTools has the Layout Shift regions overlay. The purple highlight shows the element that shifted.

The Rendering panel:
  - The Layout Shift Regions: the on
  - The Paint Flashing: the on
  - The Layer Borders: the on

The tools show the shift and the paint in the real time.

The PerformanceObserver

The API observes the metrics at the runtime.

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('The layout shift:', entry.value);
    }
  }
});

observer.observe({ type: 'layout-shift', buffered: true });

The layout-shift is the entry type, and the value is the score.

The Long Animation Frames

The API reports the frames that exceed the threshold.

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('The long frame:', entry.duration);
  }
});

observer.observe({ type: 'long-animation-frame', buffered: true });

The long-animation-frame is the entry type, and the duration is the frame length.

The will-change

The will-change promotes the element to the layer.

.card {
  will-change: transform;
}

The promotion helps the transform, and the overuse consumes the memory.

The profiling table

The toolThe purpose
The Performance panelThe trace
The Layout Shift RegionsThe shift
The Paint FlashingThe paint
The PerformanceObserverThe metrics
The will-changeThe promotion

c. The Tailwind patterns

The Tailwind utilities have the performance characteristics, and the patterns below are the ones that matter.

The transition-all

The transition-all transitions the every property. The transition of the layout property triggers the layout on the every frame.

/* ❌ The transition-all */
<div className="transition-all hover:w-64">

/* ✅ The transition-transform */
<div className="transition-transform hover:scale-110">

The transition-all is the anti-pattern, and the specific transition is the fix.

The transition properties

The Tailwind’s transition is the curated list: the color, the background-color, the border-color, the text-decoration-color, the fill, the stroke, the opacity, the box-shadow, and the transform. The transition-all is the everything.

The animation

The animate-* runs the @keyframes. The keyframe that animates the transform and the opacity is the compositor-friendly. The keyframe that animates the width and the height triggers the layout on the every frame.

/* ✅ The compositor-friendly */
<div className="animate-pulse">

/* ❌ The layout-triggering */
<div className="animate-[grow_2s_infinite]">
/* The grow animates the width. */

The aspect-ratio

The aspect-ratio reserves the space for the image. The image without the dimensions triggers the CLS. The Tailwind’s aspect-* is the fix.

<img className="aspect-video w-full object-cover" src="..." />

The browser reserves the space, and the image loads without the shift.

The contain

The contain restricts the layout scope.

.card {
  contain: layout paint;
}

The contain: layout tells the browser the element’s layout is the independent, and the contain: paint clips the paint.

The content-visibility

The content-visibility: auto skips the rendering of the off-screen content.

<section className="content-auto">
  /* The content is the rendered when the scrolled into the view. */
</section>

The content-visibility reduces the initial layout and the paint.

The will-change in Tailwind

The Tailwind’s will-change-* is the utility.

<div className="will-change-transform">

The utility sets the will-change: transform. The promotion is the helpful for the continuous animation, and the overuse is the memory cost.

The pattern table

The patternThe effect
The transition-allThe anti-pattern
The transition-transformThe compositor
The animate-pulseThe compositor
The animate-[grow]The layout
The aspect-videoThe CLS fix
The contain-layoutThe scope
The content-visibilityThe skip
The will-change-transformThe promotion

Complete Example Session

// ============================================
// PART 1: THE TRANSITION ALL ANTI-PATTERN
// ============================================

/* ❌ The transition-all */
<div className="transition-all duration-300 hover:w-64">

/* ✅ The transition-transform */
<div className="transition-transform duration-300 hover:scale-110">

// ============================================
// PART 2: THE SPECIFIC TRANSITION
// ============================================

<button className="transition-colors duration-200 hover:bg-blue-700">

// ============================================
// PART 3: THE ANIMATION
// ============================================

/* ✅ The compositor-friendly */
<div className="animate-pulse">

/* ❌ The layout-triggering */
<div className="animate-[grow_2s_infinite]">

// ============================================
// PART 4: THE ASPECT RATIO
// ============================================

<img className="aspect-video w-full object-cover" src="..." alt="" />

// ============================================
// PART 5: THE CONTAIN
// ============================================

<div className="[contain:layout_paint]">

// ============================================
// PART 6: THE CONTENT VISIBILITY
// ============================================

<section className="[content-visibility:auto]">

// ============================================
// PART 7: THE WILL CHANGE
// ============================================

<div className="will-change-transform">

// ============================================
// PART 8: THE PERFORMANCE OBSERVER
// ============================================

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('The layout shift:', entry.value);
    }
  }
});

observer.observe({ type: 'layout-shift', buffered: true });

// ============================================
// PART 9: THE LONG ANIMATION FRAME
// ============================================

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('The long frame:', entry.duration);
  }
});

observer.observe({ type: 'long-animation-frame', buffered: true });

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

// 1. The browser runs the four stages.
// 2. The layout is the expensive.
// 3. The paint is the cheaper.
// 4. The composite is the cheapest.
// 5. The transition-all is the anti-pattern.
// 6. The transition-transform is the compositor.
// 7. The animate-pulse is the compositor.
// 8. The aspect-ratio fixes the CLS.
// 9. The contain scopes the layout.
// 10. The content-visibility skips the off-screen.

The ten parts showed the transition-all anti-pattern, the specific transition, the animation, the aspect-ratio, the contain, the content-visibility, the will-change, the PerformanceObserver, the Long Animation Frames, and the complete pattern.


Quick Reference

The Render Pipeline

The stageThe propertiesThe cost
The styleThe selectorsThe O(elements)
The layoutThe geometryThe O(affected)
The paintThe visualThe O(area)
The compositeThe transform, the opacityThe O(layers)

The Property Stages

The propertyThe stage
width, heightThe layout
margin, paddingThe layout
font-sizeThe layout
background-colorThe paint
box-shadowThe paint
transformThe composite
opacityThe composite

The Profiling Tools

The toolThe purpose
The Performance panelThe trace
The Layout Shift RegionsThe shift
The Paint FlashingThe paint
The PerformanceObserverThe metrics
The will-changeThe promotion

The Tailwind Patterns

The patternThe effect
The transition-allThe anti-pattern
The transition-transformThe compositor
The animate-pulseThe compositor
The aspect-videoThe CLS fix
The contain-layoutThe scope
The content-visibilityThe skip

The Metrics

The metricThe meaning
The CLSThe layout shift
The LCPThe largest paint
The INPThe interaction
The LoAFThe long frame

Best Practices

✅ Do This:

/* Use the specific transition. */
<div className="transition-transform hover:scale-110">  /* ✅ */
/* Use the compositor-friendly animation. */
<div className="animate-pulse">  /* ✅ */
/* Use the aspect-ratio. */
<img className="aspect-video w-full object-cover" />  /* ✅ */
/* Use the contain. */
<div className="[contain:layout_paint]">  /* ✅ */
/* Use the content-visibility. */
<section className="[content-visibility:auto]">  /* ✅ */
/* Use the will-change sparingly. */
<div className="will-change-transform">  /* ✅ */
/* Measure the layout shift. */
observer.observe({ type: 'layout-shift', buffered: true });  /* ✅ */

❌ Don’t Do This:

/* Don't use the transition-all. */
<div className="transition-all hover:w-64">  /* ❌ The layout */
/* Don't animate the layout property. */
<div className="animate-[grow_2s]">  /* ❌ The width */
/* Don't leave the image without the dimensions. */
<img src="..." />  /* ❌ The CLS */
/* Don't overuse the will-change. */
<div className="will-change-transform will-change-opacity">  /* ⚠️ The memory */
/* Don't use the complex selector. */
<div className="[&_.nested]:p-4">  /* ⚠️ The style cost */
/* Don't forget the contain for the isolated. */
<div className="card">  /* ⚠️ The layout scope */

Common Pitfalls

The pitfallThe reasonThe fix
The jankThe transition-allUse the specific
The layout thrashThe animated geometryUse the transform
The CLSThe image without the dimsUse the aspect-ratio
The paint costThe large areaUse the contain
The style costThe complex selectorUse the single-class
The memory costThe will-change overuseRemove it
The off-screen costThe content-visibilityAdd the auto

Real-World Examples

1. The Specific Transition

<button className="transition-colors hover:bg-blue-700">

2. The Transform

<div className="transition-transform hover:scale-110">

3. The Animation

<div className="animate-pulse">

4. The Aspect Ratio

<img className="aspect-video w-full object-cover" />

5. The Contain

<div className="[contain:layout_paint]">

6. The Content Visibility

<section className="[content-visibility:auto]">

7. The Will Change

<div className="will-change-transform">

8. The PerformanceObserver

observer.observe({ type: 'layout-shift', buffered: true });

9. The Long Animation Frames

observer.observe({ type: 'long-animation-frame', buffered: true });

10. The Complete Pattern

<div className="transition-transform hover:scale-110">

Visual

The Render Pipeline

┌────────────────────────────────────────────────────────────┐
│  THE RENDER PIPELINE                                       │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE STYLE                                         │     │
│  │    The selectors, the cascade.                     │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE LAYOUT                                        │     │
│  │    The geometry.                                   │     │
│  │    The width, the height, the margin.              │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE PAINT                                         │     │
│  │    The visual.                                     │     │
│  │    The color, the background, the shadow.          │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE COMPOSITE                                     │     │
│  │    The transform, the opacity.                     │     │
│  │    The compositor thread.                          │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The earlier stage is the more expensive.                  │
└────────────────────────────────────────────────────────────┘

The transition-all

┌────────────────────────────────────────────────────────────┐
│  THE transition-all                                        │
│                                                            │
│  THE SOURCE                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <div class="transition-all hover:w-64">           │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE TRANSITION                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The width is the transitioned.                    │     │
│  │  The every frame: the layout.                      │     │
│  │  The main thread is the busy.                      │     │
│  │  The jank.                                         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE FIX                                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <div class="transition-transform hover:scale-110">│     │
│  │    → the transform is the compositor-only.         │     │
│  │    → the main thread is the free.                  │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The CLS

┌────────────────────────────────────────────────────────────┐
│  THE CLS                                                   │
│                                                            │
│  THE WITHOUT                                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <img src="hero.jpg" />                            │     │
│  │                                                   │     │
│  │  The image loads.                                  │     │
│  │  The layout shifts.                                │     │
│  │  The CLS: 0.25                                     │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE WITH                                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <img class="aspect-video w-full" src="hero.jpg" />│     │
│  │                                                   │     │
│  │  The space is reserved.                            │     │
│  │  The image loads.                                  │     │
│  │  The layout does not shift.                        │     │
│  │  The CLS: 0                                         │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Profiling

┌────────────────────────────────────────────────────────────┐
│  THE PROFILING                                             │
│                                                            │
│  THE DEVTOOLS                                              │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The Performance panel: the trace.                 │     │
│  │  The Layout Shift Regions: the purple.             │     │
│  │  The Paint Flashing: the green.                    │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE RUNTIME                                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The PerformanceObserver:                          │     │
│  │    The layout-shift                                │     │
│  │    The long-animation-frame                        │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The measurement is the first step.                        │
└────────────────────────────────────────────────────────────┘

Summary

The itemThe value
The render pipelineThe style, the layout, the paint, the composite
The layoutThe geometry
The paintThe visual
The compositeThe transform, the opacity
The transition-allThe anti-pattern
The aspect-ratioThe CLS fix
The containThe scope
The content-visibilityThe skip
The PerformanceObserverThe metrics
The CLSThe layout shift

Key takeaways:

  • The browser runs the four stages on the main thread. The style, the layout, the paint, and the composite. The earlier stage is the more expensive.
  • The transform and the opacity are the compositor-friendly. The browser can animate them without the main-thread work. The rest trigger the layout or the paint.
  • The transition-all is the anti-pattern. It transitions the every property, including the layout-triggering. The specific transition-transform or the transition-colors is the fix.
  • The aspect-ratio fixes the CLS. The image without the dimensions shifts the layout when it loads. The aspect-video reserves the space.
  • The contain scopes the layout. The contain: layout paint tells the browser the element’s layout is the independent.
  • The content-visibility: auto skips the off-screen. The browser does not render the off-screen content until the scroll.
  • The will-change promotes the layer, and the overuse costs the memory. The utility is the helpful for the continuous animation, and the overuse is the leak.
  • The profiling is the measurement. The DevTools Performance panel, the Layout Shift Regions, the Paint Flashing, and the PerformanceObserver are the tools.

Remember: The CSS performance is the render pipeline. The Tailwind utilities are the declarations, and the declarations determine the stage. The transform and the opacity are the compositor, and the width, the height, and the margin are the layout. The result is the CSS that stays off the critical path, and the interface that the user perceives as the smooth.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!