Skip to content

Add experimental.lightningCssFeatures config option#90901

Merged
sokra merged 13 commits intocanaryfrom
sokra/lightningcss-feature-flafs
Mar 11, 2026
Merged

Add experimental.lightningCssFeatures config option#90901
sokra merged 13 commits intocanaryfrom
sokra/lightningcss-feature-flafs

Conversation

@sokra
Copy link
Member

@sokra sokra commented Mar 4, 2026

What?

Adds a new experimental.lightningCssFeatures config option that lets users control which CSS features lightningcss should always transpile (include) or never transpile (exclude), regardless of browserslist targets.

// next.config.js
module.exports = {
  experimental: {
    useLightningcss: true,
    lightningCssFeatures: {
      include: ['light-dark', 'oklab-colors'],
      exclude: ['nesting'],
    },
  },
}

Why?

Currently, lightningcss feature transpilation is determined solely by browserslist targets. There's no way to force transpilation of a specific feature (e.g., light-dark()) when targeting modern browsers that already support it, or to skip transpilation of a feature that the user wants to preserve as-is.

This is useful for:

  • Forcing polyfill-style transpilation for features with incomplete browser support
  • Testing transpiled output for specific features
  • Opting out of specific transforms that may interfere with CSS tooling downstream

How?

TypeScript config & validation:

  • config-shared.ts: LIGHTNINGCSS_FEATURE_NAMES const array (single source of truth) with LightningCssFeature type derived from it, and LightningCssFeatures interface
  • config-schema.ts: Zod validation using z.enum(LIGHTNINGCSS_FEATURE_NAMES) for both include/exclude arrays

Feature name → bitmask mapping (Rust, shared via NAPI):

  • crates/next-core/src/next_config.rs: lightningcss_feature_names_to_mask() maps feature name strings to lightningcss::targets::Features bitflag constants and returns a Result<u32> bitmask. Unknown feature names produce an error via bail!.
  • Exposed to JS via NAPI as lightningcssFeatureNamesToMaskNapi, callable through bindings.css.lightning.featureNamesToMask(names)
  • Single source of truth for feature name resolution — both webpack and Turbopack paths use this Rust function

Webpack path:

  • global.ts / modules.ts pass config through to loader options
  • loader.ts calls featureNamesToMask() via the native bindings to compute include/exclude masks, then passes them to the SWC transform() call
  • The lightningCssFeatures without useLightningcss warning only shows when using webpack (Turbopack always uses lightningcss)

Turbopack path (Rust):

  • next_config.rs: LightningCssFeatures struct for deserialization, accessor methods converting names → u32 bitmask via lightningcss_feature_names_to_mask()
  • Config flows through CssOptionsContextModuleType::CssCssModuleAsset (using LightningCssFeatureFlags { include, exclude } struct) → process.rs where bitmasks are merged into lightningcss::targets::Targets { include, exclude }
  • Uses u32 bitmask representation throughout to avoid adding lightningcss dependency to non-CSS crates

Defaults preserved: Nesting | MediaRangeSyntax for Turbopack, Nesting for Webpack. User include is OR-ed on top, user exclude masks bits off.

Feature names: 21 individual features (bit 0–20) + 3 composite groups (selectors, media-queries, colors), all in dash-case.

Test

  • Include test (test/e2e/app-dir/experimental-lightningcss-features/) — targets Chrome 123 (which natively supports light-dark()) with include: ['light-dark'], then asserts the CSS output contains lightningcss transpilation markers (--lightningcss-light, --lightningcss-dark) instead of raw light-dark().
  • Exclude test (test/e2e/app-dir/experimental-lightningcss-features-exclude/) — targets Chrome 100 (which does NOT support light-dark() natively) with exclude: ['light-dark'], then asserts the CSS output preserves raw light-dark() calls and does not contain transpilation markers.

@nextjs-bot nextjs-bot added created-by: Turbopack team PRs by the Turbopack team. tests Turbopack Related to Turbopack with Next.js. type: next labels Mar 4, 2026
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Mar 4, 2026

Tests Passed

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 4, 2026

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing sokra/lightningcss-feature-flafs (84c68b0) with canary (ff36c17)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@nextjs-bot nextjs-bot added the Documentation Related to Next.js' official documentation. label Mar 5, 2026
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Mar 5, 2026

Stats from current PR

🔴 1 regression

Metric Canary PR Change Trend
node_modules Size 477 MB 477 MB 🔴 +57.4 kB (+0%) ████▁
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 455ms ▁▁▅█▁
Cold (Ready in log) 436ms 435ms ▁▁▄█▁
Cold (First Request) 1.238s 1.264s ▁▁▃█▁
Warm (Listen) 456ms 457ms ▁▁▅█▁
Warm (Ready in log) 445ms 439ms ▁▁▃█▁
Warm (First Request) 343ms 343ms ▁▁▃█▁
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 455ms ▁▁█▁▁
Cold (Ready in log) 437ms 436ms ▁▆█▁▄
Cold (First Request) 1.936s 1.921s ▁▄█▁▃
Warm (Listen) 455ms 455ms ▁▁█▁▁
Warm (Ready in log) 438ms 437ms ▁▄█▁▅
Warm (First Request) 1.942s 1.932s ▁▃█▁▄

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.787s 3.782s ▁▁▃█▁
Cached Build 3.844s 3.865s ▁▁▃█▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.032s 14.060s ▁▁█▁▁
Cached Build 14.149s 14.143s ▁▁█▁▁
node_modules Size 477 MB 477 MB 🔴 +57.4 kB (+0%) ████▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **402 kB** → **402 kB** ⚠️ +12 B

80 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 766 B 765 B
Total 766 B 765 B ✅ -1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 446 B 450 B
Total 446 B 450 B ⚠️ +4 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 59.4 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 254 B 253 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.6 kB -
9544-HASH.js gzip N/A 60.2 kB -
Total 233 kB 234 kB ⚠️ +734 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.51 kB 2.51 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.98 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 255 kB 256 kB
Total 380 kB 381 kB ⚠️ +887 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 619 B 615 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.8 kB 43.9 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.4 kB 45.6 kB ⚠️ +121 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.06 MB 4.07 MB 🔴 +7.8 kB (+0%)
index.pack gzip 102 kB 102 kB
index.pack.old gzip 102 kB 103 kB 🔴 +1.09 kB (+1%)
Total 4.27 MB 4.28 MB ⚠️ +9.16 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 322 kB 322 kB
app-page-exp..prod.js gzip 171 kB 171 kB
app-page-tur...dev.js gzip 322 kB 322 kB
app-page-tur..prod.js gzip 171 kB 171 kB
app-page-tur...dev.js gzip 318 kB 318 kB
app-page-tur..prod.js gzip 169 kB 169 kB
app-page.run...dev.js gzip 319 kB 319 kB
app-page.run..prod.js gzip 169 kB 169 kB
app-route-ex...dev.js gzip 70.9 kB 70.9 kB
app-route-ex..prod.js gzip 49.3 kB 49.3 kB
app-route-tu...dev.js gzip 70.9 kB 70.9 kB
app-route-tu..prod.js gzip 49.3 kB 49.3 kB
app-route-tu...dev.js gzip 70.5 kB 70.5 kB
app-route-tu..prod.js gzip 49 kB 49 kB
app-route.ru...dev.js gzip 70.4 kB 70.4 kB
app-route.ru..prod.js gzip 49 kB 49 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.9 kB 32.9 kB
pages-turbo....dev.js gzip 52.6 kB 52.6 kB
pages-turbo...prod.js gzip 38.5 kB 38.5 kB
pages.runtim...dev.js gzip 52.6 kB 52.6 kB
pages.runtim..prod.js gzip 38.5 kB 38.5 kB
server.runti..prod.js gzip 62 kB 62 kB
Total 2.84 MB 2.84 MB ⚠️ +5 B
📝 Changed Files (2 files)

Files with changes:

  • pages-api.ru..time.prod.js
  • pages.runtime.prod.js
View diffs
pages-api.ru..time.prod.js

Diff too large to display

pages.runtime.prod.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/e4dac71c3e18e45a64e90f0f09d3aef2e7efd09e/next

@sokra sokra force-pushed the sokra/lightningcss-feature-flafs branch 2 times, most recently from a9a140c to 75a5d89 Compare March 6, 2026 07:25
@sokra sokra requested a review from mischnic March 6, 2026 07:25
@sokra sokra marked this pull request as ready for review March 6, 2026 07:25
@vercel
Copy link
Contributor

vercel bot commented Mar 6, 2026

Notifying the following users due to files changed in this PR based on this repo's notify modifiers:

@timneutkens, @ijjk, @shuding, @huozhi:

packages/next/src/server/config.ts

@Strernd
Copy link
Contributor

Strernd commented Mar 6, 2026

Offers a solution for #82559

@mondyzzz-glitch
Copy link

感谢反馈!我们看到了你的feature请求。我们会尽快处理。

sokra added 6 commits March 11, 2026 05:51
Add `lightningCssFeatures: { include?, exclude? }` to experimental config,
allowing users to force-enable or disable specific LightningCSS feature
transformations regardless of browserslist targets. Supports both Turbopack
(Rust) and Webpack (JS loader) paths.
- Extract LIGHTNINGCSS_FEATURE_NAMES const array in config-shared.ts,
  derive the type from it and reuse in config-schema.ts (eliminates
  duplicated 24-item enum list)
- Extract lightningcss_features_field_mask() helper in Rust to deduplicate
  include/exclude accessor methods
- Hoist feature mask computation out of transform() call in loader.ts,
  avoiding a duplicated exclude mask calculation and an inline IIFE
- Add doc comments to bitmask mapping tables and process.rs parameters
- Simplify test by extracting collectPageCss() helper and using matchAll
- Add cross-reference comments linking the triplicated bitmask tables
  (config-shared.ts, features.ts, next_config.rs)
- Warn when lightningCssFeatures is set without useLightningcss
- Move test from test/development/ to test/e2e/ for prod-mode coverage
- Add comment clarifying CSS link regex handles query strings
- Commit tsconfig.json in test fixture (matching sibling test convention)
- Use lightningcss crate `Features` constants instead of hardcoded bitmask values
- Expose `featureNamesToMask` from Rust via NAPI, remove duplicated JS `features.ts`
- Group `lightningcss_include_features`/`lightningcss_exclude_features` into `LightningCssFeatureFlags` struct
sokra added 7 commits March 11, 2026 05:51
…ng webpack

Turbopack always uses lightningcss, so the warning is irrelevant there.
Targets Chrome 100 (which does not support light-dark() natively) with
exclude: ['light-dark'], then asserts that the CSS output preserves raw
light-dark() calls instead of transpiling them.
Use nextConfig in nextTestSetup to vary config per describe block instead
of maintaining separate fixture directories.
@sokra sokra force-pushed the sokra/lightningcss-feature-flafs branch from 821aa81 to 84c68b0 Compare March 11, 2026 05:52
@sokra sokra merged commit 187f35f into canary Mar 11, 2026
160 of 162 checks passed
@sokra sokra deleted the sokra/lightningcss-feature-flafs branch March 11, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

created-by: Turbopack team PRs by the Turbopack team. Documentation Related to Next.js' official documentation. tests Turbopack Related to Turbopack with Next.js. type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants