---
title: Optimize CSS output after moving Next.js from webpack to Turbopack
description: Diagnose larger CSS after moving Next.js to Turbopack. Use the agent skill to compare route-level output, identify causes, and test targeted fixes.
url: "https://vercel.com/kb/guide/optimize-turbopack-css"
published: 2026-10-21
last_updated: 2026-10-22
authors: Darek Rossman, Forward Deployed Engineering Team
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Turbopack can change both the size of your CSS and the stylesheets each route loads. Compare webpack and Turbopack builds from the same commit, then check browser-target rewrites, Sass-generated utilities, and CSS chunking before changing your source.

The `optimize-turbopack-css` skill guides your coding agent through those measurements and targeted fixes. Its workflow includes checks for browser compatibility, right-to-left (RTL) layouts, and styles used in content management system (CMS) output.

Turbopack is the default bundler in Next.js 16. If you're also upgrading Next.js, follow the [upgrade guide](https://nextjs.org/docs/app/guides/upgrading/version-16) and check the [known differences from webpack](https://nextjs.org/docs/app/api-reference/turbopack#known-gaps-with-webpack).

## Which apps are most likely to see larger CSS?

Pay particular attention to apps that generate utility classes with Sass loops, target older browsers, support RTL layouts, or use a shared CMS component registry. These patterns can increase generated CSS or the styles a route loads.

Tailwind CSS generates utilities from detected or explicitly included classes, which can limit the growth seen in Sass loops that emit every combination. This also applies to shadcn/ui projects using Tailwind. Older browser targets can still expand logical utilities such as `ms-*`, `me-*`, `ps-*`, and `pe-*`.

Tailwind v4 has its own [browser requirements](https://tailwindcss.com/docs/compatibility#browser-support). Setting an older Browserslist does not establish support below those versions. CSS chunking can also load unused styles, so measure per-route output even when logical properties stay unchanged.

## Why does CSS get bigger after moving to Turbopack?

Turbopack handles CSS differently from webpack in four ways. Each one can add bytes on its own or multiply the cost of another.

### Turbopack rewrites modern CSS for older browsers

Turbopack compiles stylesheets with Lightning CSS, which uses your [browser targets](https://nextjs.org/docs/architecture/supported-browsers) to decide which supported CSS features to rewrite, or "lower." Next.js's default webpack pipeline uses PostCSS for vendor prefixes and selected syntax transforms. Custom PostCSS plugins or webpack's `useLightningcss` option can change the comparison.

Logical properties such as `margin-inline-start` adapt spacing to text direction and can expand into many fallback selectors. With Next.js 15.5's default targets, this 31-byte utility expands to six rules totaling 1,401 bytes. The output excerpt shortens the language list:

```text
/* Input: 31 bytes */
.ms-3{margin-inline-start:1rem}

/* Output: six rules like this one, 1,401 bytes in total (language list shortened) */
.ms-3:not(:is(:lang(ae),:lang(ar),…,:lang(yi))){margin-left:1rem}
```

CSS excerpt with shortened language list

This logical-property rewrite can change behavior as well as size. The added pseudo-classes increase specificity, so generated rules can override declarations that previously took precedence.

When the targets also lack `:dir()` support, Lightning CSS approximates direction with `:lang()` selectors. Changing `dir` without a corresponding language change can then leave the spacing pointing the wrong way.

Next.js 15.5's default targets include browsers as old as Chrome 64 and Safari 12, which trigger this fallback chain. Next.js 16 raised its default targets, so most apps using those defaults avoid logical-property lowering. Custom Browserslist settings can still trigger it, and the Next.js 16 defaults still lower some newer features, such as `light-dark()` and `oklch()` colors.

### Sass loops and per-file minification multiply the cost

Sass generators can repeat logical-property rewrites across every breakpoint and size they emit. Even a small loop can therefore produce a large stylesheet.

In this article's reproduction, a 704-byte Sass loop emitted 144 logical spacing utilities. The simplified version below shows how it combines breakpoints, logical properties, and spacer sizes:

```text
$breakpoints: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px);
$spacers: (0: 0, 1: .25rem, 2: .5rem, 3: 1rem, 4: 1.5rem, 5: 3rem);
$properties: (ms: margin-inline-start, me: margin-inline-end, ps: padding-inline-start, pe: padding-inline-end);

/* 6 breakpoints × 4 logical properties × 6 spacers = 144 utilities */
@each $bp, $min in $breakpoints {
  @media (min-width: $min) {
    @each $abbr, $prop in $properties {
      @each $key, $value in $spacers {
        .#{$abbr}-#{$bp}-#{$key} { #{$prop}: $value !important; }
      }
    }
  }
}
```

With Next.js 15 targets, the webpack output was about 12 KB and the Turbopack output about 205 KB, a 17.7-fold increase. Gzip output grew 5.3-fold. Compression reduces transfer size, but browsers still process the expanded CSS.

| Build                         | Rules | Raw      | Gzip   |
| ----------------------------- | ----- | -------- | ------ |
| webpack, Next.js 15 targets   | 144   | 11.6 KB  | 0.8 KB |
| Turbopack, Next.js 15 targets | 864   | 205.4 KB | 4.1 KB |

Shared Sass partials add a second multiplier. When several CSS Modules load a partial that emits CSS, each separately compiled module includes that output. Webpack minifies the combined output of each CSS chunk and can remove identical rules within it. Turbopack minifies the modules separately before combining them, so repeated rules can survive across modules.

For example, this partial mixes a variable with a rule that emits CSS:

```text
$heading-font: 'Brand Sans', sans-serif;

@font-face {
  font-family: 'Brand Sans';
  src: url('/fonts/brand-sans.woff2') format('woff2');
}
```

This CSS Module imports the partial for its font variable:

```text
@use '../styles/fonts';

.title {
  font-family: fonts.$heading-font;
}
```

Every CSS Module that loads `_fonts.scss` emits a copy of the `@font-face` rule. Move `@font-face` rules into a stylesheet that loads once. Keep shared partials to variables, functions, and mixin definitions that emit no CSS when loaded.

### Class name length with CSS Modules

Through Next.js 16.3, Turbopack uses longer CSS Module class names than webpack, such as `Details-module-scss-module__MGoXJG__label` for a Sass module.

These names repeat in your CSS, in every `class` attribute of server-rendered HTML, and in the React Server Components (RSC) payload, adding raw bytes that compression mostly hides. Next.js 16.4 [shortens these names in production builds](https://github.com/vercel/next.js/pull/97944).

### Turbopack splits and delivers CSS differently

Turbopack preserves stylesheet order when grouping CSS into shared chunks because order can affect the cascade. The grouping makes a [trade-off between request count and download size](https://github.com/vercel/next.js/discussions/95167), so routes can receive styles they don't use.

Next.js 16.3 added [`experimental.cssChunking: 'graph'`](https://nextjs.org/docs/app/api-reference/config/next-config-js/cssChunking), an experimental Turbopack-only option that changes this grouping. Compare route bytes and request counts before adopting it. From that version, Turbopack rejects the webpack-only values `'strict'` and `false`, so remove either setting if it remains in your configuration.

Statically imported component registries, error boundaries, and not-found pages can also attach their CSS to routes that never render them. Some of these behaviors also happen under webpack, but a migration is often when teams first measure them. As a result, total CSS can shrink after a migration while individual pages get heavier.

## Optimizing Turbopack CSS

The `optimize-turbopack-css` skill includes a diagnostic workflow, notes on Next.js 15 and 16, and a dependency-free analyzer. Use it to investigate the causes that apply to your app and measure each change separately. You can request analysis only, a plan, or changes one phase at a time.

### 1\. Establish comparable baselines

Build webpack and Turbopack from the same commit with matching dependencies, environment, browser targets, and production settings. Preserve each build's output separately so the next build doesn't overwrite your baseline.

Compare the CSS each route loads alongside the build totals. Use route manifests to identify associated assets, then check actual CSS requests during cold browser navigations. Manifest entries alone don't establish what the browser downloads.

The analyzer counts selectors and transformation markers to help identify lowering. An isolated compilation of the simplified Sass loop above with Lightning CSS 1.33.0 produced this output:

```text
files=1 raw=200.5 KiB gzip=4.0 KiB brotli=1.8 KiB
rules=864 selectors=864 selectorChars=188496
markers lang=16416 dir=0 is=288 webkitAny=288 mozAny=288 lightDark=0 lab=0 logical=0
```

### 2\. Isolate browser-target rewrites

Run a temporary build with modern browser targets to test their effect on output. These POSIX shell commands override Browserslist for one build without editing your configuration. Treat the result as diagnostic output until any change to supported browsers has been approved.

```bash
# Next.js 16
BROWSERSLIST="chrome 111, edge 111, firefox 111, safari 16.4" npx next build

# Next.js 15.5
BROWSERSLIST="chrome 111, edge 111, firefox 111, safari 16.4" npx next build --turbopack
```

Then inspect Sass generators and shared partials, global framework styles, route delivery, and CSS Module class names. Selector markers help distinguish lowering from other repetitive output.

### 3\. Change one cause at a time

Inventory class names used by CMS content or third-party HTML before removing selectors. Measure each fix against its baseline. Typical changes restrict Sass loops to needed utilities, keep shared partials free of emitted rules, and move feature CSS to the routes that use it.

On Next.js 16.2 and later, [`experimental.lightningCssFeatures`](https://nextjs.org/docs/app/api-reference/config/next-config-js/useLightningcss) can exclude specific rewrites. The following `next.config.ts` settings remove the compatibility fallbacks for logical properties and `:dir()` selectors. Use them only if the affected CSS works in your supported browsers, either natively or through fallbacks you supply.

```typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    lightningCssFeatures: { exclude: ['logical-properties', 'dir-selector'] },
  },
}

export default nextConfig
```

### 4\. Validate the rendered result

Check cascade order and the layouts your app supports, including RTL, localized content, and print styles where applicable. Exercise CMS content and runtime-generated class names. Add per-route CSS budgets to continuous integration (CI) to detect later growth.

## CSS reductions in two production apps

We used the workflow with two enterprise teams. The marketing site needed to reduce the CSS delivered to individual routes. The commerce app needed to reduce repetitive fallback rules generated from Sass utilities.

### Marketing site: pages loaded CSS they didn't use

The first project was a CMS-driven marketing site on Next.js 16.3. Total CSS dropped from 462 KiB under webpack to 360 KiB under Turbopack. The median route still loaded about 340 KiB, including styles for blocks it never rendered.

We traced the excess route CSS to a registry that imported every CMS block component statically. The investigation also found a shared Sass partial that emitted `@font-face` rules into every CSS Module that imported it.

The team made four changes:

- The registry loads blocks through `next/dynamic` boundaries with server rendering enabled. In this app, the change reduced block CSS loaded by routes that did not render those blocks.
  
- The `@font-face` rules moved from the shared Sass partial into one global stylesheet.
  
- The team enabled `experimental.cssChunking: 'graph'` to reduce unused CSS delivered across routes further.
  
- Splitting block styles exposed two components that depended on stylesheet order. Explicit component overrides fixed both.
  

| Initial route CSS          | Turbopack baseline | Optimized Turbopack | Reduction |
| -------------------------- | ------------------ | ------------------- | --------- |
| Homepage, raw              | 250.5 KiB          | 139.4 KiB           | 44%       |
| Homepage, gzip             | 33.5 KiB           | 23.1 KiB            | 31%       |
| Median of 290 routes, raw  | 339.8 KiB          | 111.4 KiB           | 67%       |
| Median of 290 routes, gzip | 51.3 KiB           | 18.4 KiB            | 64%       |

No measured route got larger in raw, gzip, or Brotli bytes. The trade-off was more, smaller requests, with the homepage's initial stylesheets growing from 4 to 9.

### Commerce monorepo: Sass generators multiplied by lowering

The second project was an enterprise commerce monorepo on Next.js 15.5. It combines Bootstrap 5.3 with an in-house utility library and supports many locales with dedicated fonts. It also switches text direction at runtime and renders CMS HTML from outside the repository.

Raw CSS grew 134% on the first Turbopack build, while gzip grew only 8% and Brotli shrank 3%. That gap suggested repetitive output. Inspecting the emitted CSS identified logical-property fallbacks generated from the app's Sass utilities.

The team measured each change separately:

1. Moving Browserslist to the Next.js 16 defaults while remaining on Next.js 15.5 reduced raw CSS by 53%. The team treated this as a separate browser-support decision requiring explicit approval.
   
2. The breakpoint-by-size matrix of font utilities became one non-responsive set. The Sass guard now blocks new breakpoint copies.
   
3. Bootstrap's utility API took over spacing, remapped to logical properties for runtime direction switching. The team grouped language selectors and changed components to import token-only Sass entry points.
   
4. The root error boundary became self-contained, and web-component registration moved from the root layout to the localized layout.
   

| Build (all CSS)                           | Raw      | Gzip   | Brotli |
| ----------------------------------------- | -------- | ------ | ------ |
| webpack baseline                          | 1,534 KB | 247 KB | 198 KB |
| First Turbopack build                     | 3,595 KB | 266 KB | 192 KB |
| Turbopack with Next.js 16 browser targets | 1,673 KB | 216 KB | 170 KB |
| Final optimized Turbopack                 | 1,424 KB | 203 KB | 168 KB |

The final build is 7% smaller than the webpack baseline in raw bytes, 18% smaller in gzip, and 15% smaller in Brotli.

Before pruning utilities, the team created a safelist of class patterns used by CMS HTML. They committed the safelist with per-route CSS budgets and Sass contract tests to detect regressions as the app changes.

## Try the skill on your app

The skill is available on its development branch. With Node.js 22.20 or later, run this command from your project and choose the coding agent to install it for:

```bash
npx skills add https://github.com/vercel-labs/agent-skills/tree/add-optimize-turbopack-css-skill/skills/optimize-turbopack-css
```

For an analysis-only review, give your agent this prompt:

### Agent prompt

```txt
Our CSS grew after switching to Turbopack. Use the optimize-turbopack-css skill to compare webpack and Turbopack builds from the same commit, including total and per-route CSS sizes in raw and gzip bytes.

Identify the main causes with evidence, then recommend targeted fixes and note browser-compatibility or layout risks. Flag any measurements you cannot verify. Analyze without changing source files, configuration, or the browsers we support.
```

## More Next.js guides

- [Sitemap lastmod should track content, not deployments](/kb/guide/correct-lastmod-nextjs-sitemap): Set accurate sitemap lastmod dates in Next.js using stored content timestamps, targeted revalidation, and checks that distinguish content changes from builds.
- [Migrate self-hosted Next.js and containers from AWS to Vercel](/kb/guide/migrate-containers-from-aws-to-vercel): Migrate Next.js apps and HTTP containers from AWS to Vercel. Configure AWS access, assess background workers, and plan testing, DNS cutover, and rollback.
- [Next.js on Vercel vs Webflow Cloud](/kb/guide/next-js-on-vercel-vs-webflow-cloud): Compare running Next.js on Vercel Functions with Fluid compute against Webflow Cloud on Cloudflare Workers. Learn how Next.js feature support, runtimes, storage, pricing, and developer experience differ so you can choose the right platform.