Skip to main content
Блогқа оралу
Next.jsCSSPerformanceTurbopackSCSS

Next.js 16-да рендерлеуді бөгейтін 94 CSS файлын нашар құжатталған Turbopack мүмкіндігімен қалай жойдым

experimental.inlineCss-тен MutationObserver хактарына дейін әрбір тәсілді сынап көрген бірнеше күннен кейін мен Next.js App Router-дегі рендерлеуді бөгейтін CSS мәселесін шешетін Turbopack Import Attributes мүмкіндігін таптым.

Жарияланды 2026 ж. 8 сәуір12 мин оқу

Мәселе

Біздің қосымша (Promova) Next.js 16-ны Landing Builder — маркетингтік беттерді ~90 түрлі секция компоненттерінен (hero-лар, FAQ-тар, баға, пікірлер т.б.) құрастыратын CMS негізіндегі жүйемен бірге қолданады. Архитектура секция атауларын next/dynamic() шақыруларына сәйкестендіретін sectionRegistry.tsx файлын пайдаланады:

sectionRegistry.tsx
// sectionRegistry.tsx — imports all 90 sections
export const sectionRegistry = {
  HeroSection: dynamic(() => import('./HeroSection/HeroSection')),
  FAQSection: dynamic(() => import('./FAQSection/FAQSection')),
  ReviewsSection: dynamic(() => import('./ReviewsSection/ReviewsSection')),
  // ... 87 more
}

Бір лендинг беті небәрі 5-8 секцияны ғана рендерлейді. Бірақ Lighthouse мынаны көрсетті:

Eliminate render-blocking resources
  94 CSS resources (~330 KB)
  Potential savings: 5,440 ms

Неге? Turbopack барлық 90 import() жолын қолжетімді деп санайды және әрбір SCSS модулі үшін <link rel="stylesheet"> жасайды. Тіпті бетте ешқашан рендерленбейтін секциялардың да CSS-і <head> ішіне енгізіледі. Бұл — Next.js App Router-дің расталған, күтілетін әрекеті. CSS Server Components-тан келетін dynamic() импорттары үшін кодқа бөлінбейді. Мейнтейнерлер мұны FOUC-тың алдын алу үшін жасалған әдейі ымыраға келу деп есептейді. Түзету жоспарланбаған.

Мен сынап көрген барлық әдіс (және неге сәтсіз аяқталғаны)

Мен таба алатын әрбір тәсілді қарастырып, бірнеше күн жұмсадым. Міне толық тізім:

ApproachWhy it doesn't work
Split sectionRegistry into per-section filesCSS still loaded — #61066
experimental.inlineCss: trueInlines CSS on every SSR request — crashed our CMS under load
experimental.optimizeCss (Critters)Incompatible with streaming in App Router
CSS-in-JS rewriteRewriting 90 sections from SCSS to CSS-in-JS is not realistic
media="print" hackDoesn't work with CSS Modules in Turbopack — <link> tags are managed by the framework
Switch back to WebpackHas experiments.css options, but we're committed to Turbopack
Turbopack pluginsDon't exist — no plugin API
Turbopack CSS loadersNot supported — only JS output
SWC plugins for CSSSWC only processes JavaScript
Client Component wrapper for dynamic()Registry is a global constant — bundler sees all dependencies
Next.js middleware HTML rewriteMiddleware can't modify response body
Suspense + streaming with async importReact Float always pulls CSS into initial <head>
Suspense + 3s delayContent streams later, but CSS is in the initial <head> chunk
experimental.cssChunkingAlready default. Merges chunks but doesn't remove irrelevant CSS
Post-build Beasties/CrittersOnly for static export, not with ISR
npm libraries (next-critical, fg-loadcss)Pages Router only or abandoned (6+ years)
inlineCss per-route exclusionNot supported — all-or-nothing global flag
Turbopack as: 'raw' for SCSSReturns undefined instead of text
Turbopack rule for *.inline.module.scssTurbopack intercepts .module.scss before custom rules
Turbopack rule for *.inline.scssTurbopack intercepts .scss before custom rules
MutationObserver <script>Tested in dev, risky — CSS still loads as separate files, possible FOUC

inlineCss тұзағы

Next.js-те барлық <link rel="stylesheet"> тегтерін inline <style> тегтерімен ауыстыратын experimental.inlineCss жалауы бар. Тамаша естіледі, солай ма?

Мәселе мынада: бұл бәрі не ештеңе. Оны маршрут бойынша қосуға болмайды. Егер сізде SSR (force-dynamic) беттер болса, әрбір сұраныс барлық CSS-ті inline түрде қайта құрады. Біз оны сынап көрдік — біздің headless CMS жүктемеге шыдамай, істен шықты. Мұның қауіпсіз жұмыс істеуі үшін беттеріңіздің 100%-ы force-static немесе ISR болуы керек. 20-дан астам SSR беті (аутентификация, бақылау тақталары, динамикалық беттер) болғанда бұл — ауқымды миграция.

Ашылым: Turbopack Import Attributes

Next.js 16.2 шығарылым жазбаларын ақтарып отырып, мен нашар құжатталған мүмкіндікті таптым: Turbopack Import Attributes. Ол TC39 with {} синтаксисін қолданып, нақты бір импорт үшін бұрыннан бар бандлер конвейерін ауыстыруға мүмкіндік береді:

import { cssText, styles } from './hero.module.scss' with {
  turbopackLoader: '@promova/scss-to-inline-loader',
  turbopackAs: '*.js'
}

Бұл Turbopack-ке былай дейді: "Бұл импортты стиль кестесі ретінде өңдеме. Оны менің арнайы loader-ым арқылы өткізіп, нәтижесін JavaScript ретінде қарастыр."

Бұл — түйінді түсінік. Turbopack рендерлеуді бөгейтін <link rel="stylesheet"> жасаудың орнына, біздің loader SCSS-ті компиляциялап, оны JS жолы ретінде экспорттайды. Содан кейін біз оны компоненттің ішіне тікелей inline <style> тегі ретінде енгіземіз. Нәтиже: бетке нақты рендерленетін секциялардың CSS-і ғана бет HTML-іне түседі. 5 секциясы бар бет 94 емес, 5 секцияға арналған CSS алады.

Шешім

1. Арнайы Turbopack Loader

yarn workspace пакеті ретіндегі ~70 жолдық Node.js скрипті (@promova/scss-to-inline-loader):

@promova/scss-to-inline-loader/index.js
const { createHash } = require('node:crypto')
const sass = require('sass')
const postcss = require('postcss')
const postcssModules = require('postcss-modules')
const path = require('path')

const SHARED_STYLES = path.resolve(__dirname, '../shared/styles')

// Same prependData as sassOptions.prependData in next.config.ts
const PREPEND_DATA = `
@use "sass:math" as math;
@use "sass:list" as list;
@use "${SHARED_STYLES}/_variables.scss" as *;
@use "${SHARED_STYLES}/_breakpoints.scss" as *;
@use "${SHARED_STYLES}/_mixins.scss" as *;
`

module.exports = function scssToInlineLoader(source) {
  const callback = this.async()
  processScss(source, this.resourcePath)
    .then((js) => callback(null, js))
    .catch((err) => callback(err))
}

async function processScss(source, resourcePath) {
  // 1. Compile SCSS → CSS (with global variables, mixins, breakpoints)
  const sassResult = sass.compileString(PREPEND_DATA + '\n' + source, {
    loadPaths: [path.dirname(resourcePath), SHARED_STYLES],
    style: 'compressed',
    sourceMap: false,
    url: new URL('file://' + resourcePath),
  })

  // 2. Scope class names with postcss-modules (CSS Modules compatible)
  let classNames = {}
  const result = await postcss([
    postcssModules({
      generateScopedName(name, filename) {
        const file = path.basename(filename, path.extname(filename))
          .replace('.module', '')
        const hash = createHash('md5')
          .update(filename + name)
          .digest('base64url')
          .slice(0, 6)
        return `${file}__${name}__${hash}`
      },
      getJSON(_, json) { classNames = json },
    }),
  ]).process(sassResult.css, { from: resourcePath })

  // 3. Export as JS module — same interface as CSS Modules
  return [
    `export const cssText = ${JSON.stringify(result.css)};`,
    `export const styles = ${JSON.stringify(classNames)};`,
    `export default styles;`,
  ].join('\n')
}

Ол не істейді: styles — стандартты CSS Modules-тегідей аймақталған класс атауларының картасы. cssText — жол ретіндегі компиляцияланған CSS.

2. InlineStyle компоненті

Автоматты дедупликация үшін React 19-дың кірістірілген <style href precedence> API-ін пайдаланады:

InlineStyle.tsx
export function InlineStyle({ css, id }: { css: string; id: string }) {
  return (
    <style href={id} precedence="default">
      {css}
    </style>
  )
}

React 19 кепілдік береді: бірдей href → DOM-да тек бір <style>. Егер секция бетте екі рет рендерленсе, CSS бәрібір бір рет қана енгізіледі.

3. Компонент бойынша миграция (секциясына ~6 жол)

// BEFORE: Turbopack → <link rel="stylesheet"> (render-blocking)
import styles from './hero_section.module.scss'
HeroSection.tsx
// AFTER: Custom loader → JS module → inline <style> (non-blocking)
import { cssText, styles } from './hero_section.module.scss' with {
  turbopackLoader: '@promova/scss-to-inline-loader',
  turbopackAs: '*.js'
}
import { InlineStyle } from '@promova/scss-to-inline-loader/InlineStyle'

export function HeroSection() {
  return (
    <section className={styles.hero}>
      <InlineStyle css={cssText} id="hero-section" />
      {/* ... component JSX, className usage is identical */}
    </section>
  )
}

.module.scss файлдары дәл сол күйінде қалады. CSS-ті қайта жазу жоқ. Stylelint, Prettier, IDE қолдауы — бәрі сақталады.

Неге бұл inlineCss: true-ден жақсы

Міне басты айырмашылық:

<code>inlineCss: true</code>Import Attributes + Loader
What gets inlinedALL CSS on the page (94 files)Only CSS for rendered sections (5-8 files)
SSR overheadCSS rebuilt on every requestCSS compiled at build time
Per-route controlNo (all-or-nothing)Yes (per-import)
SSR pages safetyRisky (overloads server)Safe (component-level)

inlineCss: true кезінде бет бәрібір БАРЛЫҚ 94 стиль кестесін inline түрде алады — олар жай ғана <link> тегтерінің орнына <style> тегтері. HTML ісінеді. Біздің тәсілмен тек нақты рендерленетін CSS қана HTML-ге түседі. 5 секция рендерленді → 5 <style> тегі. Болды.

Turbopack тұзағы: .module.scss үшін глобалды ережелер жоқ

Мен түскен бір тұзақ: loader-ды глобалды түрде қолдану үшін next.config.ts ішіне Turbopack ережесін қосуға болады деп ойлауыңыз мүмкін:

next.config.ts
// ❌ THIS CAUSES A FATAL PANIC
turbopack: {
  rules: {
    '**/components/**/*.module.scss': {
      loaders: ['@promova/scss-to-inline-loader'],
      as: '*.js'
    }
  }
}

Мұны істемеңіз. Turbopack-тің кірістірілген CSS модуль конвейері арнайы ережелер қолданылмас бұрын .module.scss файлдарын ұстап алады. Ол as: '*.js' шығысымен қайшы келетін EcmascriptCssModule активін жасайды, бұл мынаған әкеледі:

FATAL PANIC: inner asset should be CSS processable

with {} атрибуттары жұмыс істейді, өйткені олар Turbopack-ке импорт орнында CSS модуль конвейерін толығымен айналып өтуді тапсырады. Мұны конфигурация арқылы глобалды түрде жасаудың жолы жоқ.

Нәтижелер

Landing Builder бойынша 127 секция компоненті көшірілді. Өндірістік құрастыру тексерілді.

MetricBeforeAfter
Render-blocking CSS files940 (for landing sections)
CSS in HTML per page~330 KB (all sections)~20-40 KB (rendered sections only)
CSS delivery<link> (network request, blocking)<style> (inline, non-blocking)
Sass featuresFullFull (variables, mixins, nesting, @use)
CSS Modules scopingBuilt-inpostcss-modules (compatible)
Code change per section~6 lines (import + InlineStyle)

Шектеулер

  • Импорт сайын with {} ұзақ-сонар — әрбір импортқа 3 қосымша жол қажет. Оны глобалды түрде қолданудың жолы жоқ (жоғарыдағы тұзақты қараңыз).
  • Тек Turbopackwith {} атрибуттарын Webpack қолдамайды. Егер Webpack қосалқы нұсқасы қажет болса, импорттар стандартты CSS Modules-ке қайтады (cssText undefined болады).
  • Класс атауларын хештеу — біздің loader Turbopack-тің кірістірілгенінен өзге хештеу алгоритмін қолданады. Класс атаулары DevTools-та басқаша көрінеді. Функционалды тұрғыдан бірдей.
  • HTML өлшемі ұлғаяды — CSS бөлек файлдар ретінде кэштелудің орнына HTML ішінде inline болады. CDN-нен берілетін статикалық/ISR беттер үшін бұл мәселе емес.

Мұны қашан қолдану керек

Бұл әдіс мына жағдайларда неғұрлым тиімді:

  1. Сізде registry/barrel үлгісі бар — бір файл көптеген компоненттерді импорттайды, бірақ бетте тек бірнешеуі ғана рендерленеді
  2. Сіз Turbopack-тесіз — Import Attributes Turbopack-ке тән
  3. Сізге компонент бойынша бақылау керек — бәрі-не-ештеңе жалауы емес
  4. Сіздің SCSS күрделі — айнымалылар, mixin-дер, breakpoint-тер, ұялау — бәрі қолдау табады
  5. Сіз experimental.inlineCss-ті қолдана алмайсыз — өйткені сізде SSR беттер бар немесе егжей-тегжейлі бақылау қалайсыз

Егер сіз Next.js App Router-дегі рендерлеуді бөгейтін CSS-тен зардап шексеңіз — сіз жалғыз емессіз:

Негізгі мәселе

  • #62485 — Render blocking CSS (maintainers: "expected behavior")
  • #61066 — Dynamic imports from Server Components are not code-split
  • #54935 — Server-side dynamic imports don't split client modules
  • #61574 — JS/CSS code splitting doesn't work as documented
  • #57634 — Add support for critical CSS inlining with App Router
  • #50300 — next/dynamic on server component does not build CSS modules

inlineCss мүмкіндігі және мәселелер

  • PR #72195 — experimental: css inlining (the implementation)
  • PR #73182 — Don't inline CSS in RSC payload for client navigation
  • #75648 — WhatsApp preview broken with inlineCss + Tailwind
  • #83612 — Turbopack wrong font URL with inline CSS

Қауымдастық шешім іздеуде

Turbopack CSS қателері

  • #68412 — Incorrect CSS modules load order with external components
  • #76464 — Turbopack sometimes strips SCSS imports
  • #82497 — SCSS module not loading when not-found.tsx is present
  • #88544 — Exporting Sass variables from CSS modules doesn't work with Turbopack

Миллиондаған пайдаланушыға қызмет көрсететін тіл үйрену платформасы Promova-да жасалған. Егер сіз дәл осындай CSS мәселесімен күресіп жатсаңыз, жоғарыдағы GitHub issue-ларында осы мақалаға сілтеме жасаудан тартынбаңыз.