Skip to main content
Блогқа оралу
Next.jsPerformanceCode SplittingTurbopackReact Server Components

next/dynamic сізді құтқармайды: CMS-басқарылатын Next.js беттері неге әрбір чанкті жөнелтеді

Біздің CMS-басқарылатын Next.js беттеріміз әрбір роутта барлық ~100 секция компонентін жөнелтетін — next/dynamic оқулықтағыдай дұрыс қолданылғанына қарамастан. Біз нақты себепті тапқанша бандлер деңгейіндегі әрбір шешімді (тікелей import(), файлдарды бөлу, Turbopack конфигурациясы, webpack splitChunks) сарқа қолдандық: мәселе чанкылеуде емес, қолжетімділікте. Build-тайм кодген + rewrites first-load JS-ті 53%-ға қысқартты.

Жарияланды 2026 ж. 18 шілде14 мин оқу

Бізде шамамен 700 CMS-басқарылатын лендингі бар продакшн Next.js 16 сайты болды. Әрбір бет «секциялардан» — hero, FAQ, пікірлер, баға және т.б. — құрастырылады, барлығы 38 секция түрінде шамамен 100 React компоненті. Әдеттегі бет солардың 10–15-ін рендерлейді. Басты бет 5.3 MB first-load JavaScript жөнелтетін. Ол қайда кеткенін қазып қарағанымызда, 108 секцияның кодын қамтитын жалғыз 1.2 MB чанк таптық — 15 секцияны рендерлейтін бетте.

Әрбір секция next/dynamic ішіне әлдеқашан оралған болатын. Код бөлу оқулықтағыдай дұрыс көрінетін. Соған қарамастан бандлер бәрін, барлық жерге жөнелтетін. Біз күндер бойы әрбір құжатталған тұтқаны сынап көрдік — динамикалық импорттар, тікелей import(), файлдарды қайта ұйымдастыру, бандлер конфигурациясы, тіпті бандлерді ауыстыру — және әрқайсысы бір мардымсыз себеппен сәтсіз аяқталды. Бұл мақала әрбір тұйықты нақты сандармен қарастырады, шынайы түбегейлі себепті түсіндіреді және бірде-бір секция компонентіне тиместен first-load JS-тің 53%-ын қысқартқан шешімді көрсетеді.

TL;DR: CMS-басқарылатын динамикалық роуттарда код бөлу — чанкылеу мәселесі емес, қолжетімділік мәселесі. Бірде-бір бандлер жалаушасы оны емдемейді. Оны «бұл бет қандай компоненттерді қолданады» деген білімді рантаймнан build-таймға көшіру емдейді.

Архитектура (ол сізде де болуы ықтимал)

Кез келген page-builder үшін бұл стандартты орнату: CMS бетті секцияларға сілтемелердің реттелген тізімі ретінде сақтайды, ал қосымшада секция атауларын компоненттерге сәйкестендіретін орталық реестр бар. Әрбір жазба, дәл құжаттама ұсынғандай, next/dynamic ішіне оралған:

sectionRegistry.tsx
// One central registry: every section variant wrapped in next/dynamic
export const sectionRegistry = {
  'sections.hero': {
    concept_1: dynamic(() => import('./sections/Hero/HeroV1')),
    concept_2: dynamic(() => import('./sections/Hero/HeroV2')),
    // ...10 more hero variants
  },
  'sections.faq': {
    concept_1: dynamic(() => import('./sections/Faq/FaqV1')),
  },
  // ...38 section types, ~100 variants total
}

Серверлік компонент әрбір секцияны рендер сәтінде атауы бойынша шешеді және оны рендерлейді. Беттер — ISR-мен SSG, сондықтан осының бәрі серверде болады — клиент тек HTML-ді және гидрацияға қажет чанктерді алады:

SectionRenderer.tsx
// Server component: picks ONE section by name from CMS data
export async function SectionRenderer({ name, concept, id }) {
  const data = await fetchSectionData(name, id)
  const Component = sectionRegistry[name].concepts[concept]
  return <Component {...data} />
}

Бұл дизайн шынымен де жақсы: маркетологтар беттерді CMS ішінде деплойсыз құрастырады, бір реестр 700 бетке қызмет етеді, әрбір секция нұсқасы тәуелсіз түрде жалқау. Қағаз жүзінде. Бандл басқа әңгіме айтты.

Адал өлшеу (DevTools сізге өтірік айтады)

Кез келген түзетуден бұрын бізге сенуге болатын өлшем қажет болды. Network-панеліндегі аңғырт өлшеулерді үш нәрсе улайды:

  • Төменгі жолақтағы жалпы сандар панель жазып жатқанша жинақталып отырады. Load-тан кейін бірнеше секундта фреймворктың сілтеме префетчі БАСҚА роуттардың бандлдерін фонда тарта бастайды — бос қойындыда жалпы сандар «ақыр соңында бәрі» дегенге жинақталады да, кез келген first-load ұтысын жасырады.
  • Браузер кеңейтімдері сіздің өлшеміңізге өз скрипттерінің мегабайттарын құяды — тіпті инкогнитода да, егер оларға сонда рұқсат берілсе. Бір кеңейтімнің 2 MB «бет JS-ін» қосқанын бақыладық.
  • Кештен берілген жолдар ((disk cache) / (memory cache)) желі арқылы нөл байт тасымалдайды, сондықтан жылы қайта жүктеу мүлдем ештеңе өлшемейді.

Шынымен маңызды сан — және Core Web Vitals реакция жасайтыны — бұл бастапқы скрипт жиыны: серверде рендерленген HTML-дегі <script> тегтері. Дәл солар гидрацияны бөгейді. Оны скриптпен алу да оп-оңай:

// Count the REAL first-load JS: the <script> set of the prerendered HTML.
// (DevTools totals lie — more on that below.)
const html = fs.readFileSync('.next/server/app/en.html', 'utf8')
const scripts = [...new Set(
  [...html.matchAll(/<script src="([^"?]+\.js)[^"]*"/g)].map(m => m[1])
)]
let bytes = 0
for (const src of scripts) bytes += fs.statSync('.next' + src).size
console.log(scripts.length, 'chunks,', (bytes / 1024).toFixed(0), 'KB')

Модуль бойынша атрибуция үшін біз productionBrowserSourceMaps-ты уақытша қостық және шағын VLQ-парсермен әрбір генерацияланған байтты оның бастапқы модуліне жатқыздық. Білуге тұрарлық бір қақпан: Turbopack әрбір чанктің .map файлын .js-тен басқа хешпен атайды — js + '.map' деп болжаудың орнына чанк соңынан sourceMappingURL түсініктемесін оқыңыз.

Бес тұйық (сіз оларды қайталамауыңыз үшін)

Төмендегілердің әрқайсысы толық продакшн-билдпен және жоғарыдағы өлшеммен тексерілді. Ешқайсысы санды қозғалтпады. Дәл осы қайталанушылық — мәні: сәтсіздік режимі сіз лақтырған әрбір құралдан аман өтеді, өйткені бұл құралдардың бәрі басқа мәселені шешеді.

Тұйық №1: «жай ғана next/dynamic қолдан»

Ол әлдеқашан сонда болатын. ~100 нұсқаның әрқайсысы dynamic() ішіне оралған еді. First-load JS бәрібір 5.3 MB болды. dynamic() не уәде етсе де, ол мұнда оны бермеді — осы ойды есте сақтаңыз, себеп бір сәтте келеді.

Тұйық №2: серверлік компонентте тікелей await import()

Мүмкін мәселе next/dynamic орамасында шығар? App Router-де серверлік компонент динамикалық импортты тікелей await-тей алады — бұл кітапханаларды жалқау жүктеудің ресми паттерні. Біз dynamic() орамаларының реестрін шикі import() thunk-тарының картасына ауыстырдық:

// Dead end #2: replace next/dynamic with a direct server-side import()
const loaders = {
  'sections.hero': { concept_1: () => import('./sections/Hero/HeroV1') },
  // ...
}
export async function SectionRenderer({ name, concept, id }) {
  const data = await fetchSectionData(name, id)
  const { default: Component } = await loaders[name][concept]()
  return <Component {...data} />
}
// Result: byte-for-byte the same megachunk. Nothing changed.

Тұйық №3: реестрді 38 файлға бөлу

Келесі гипотеза: бандлер чанктерді біріктіреді, өйткені барлық ~100 import() шақыруы бір модульде тұрады. Сондықтан біз секцияның әрбір түріне бір лоадер файлын генерациялап шығардық — 38 файл, әрқайсысы тек өз нұсқаларының import() thunk-тарын, плюс оларды атауы бойынша іздейтін жұқа барельді ұстайды.

Нәтиже: байтпа-байт бірдей шығыс. Дәл сол мегачанк, дәл сол хешке жақын өлшем. Бұл шынымен пайдалы алғашқы теріс нәтиже болды: бандлерлер чанктерді сіздің import() шақыруларыңыздың файлдық орналасуы бойынша емес, модуль графы бойынша топтастырады. Мәлімдемелерді файлдар арасында жылжыту граф үшін көрінбейді — сол модульдер жиыны сол резолвердан қолжетімді болып қала береді.

Тұйық №4: бандлер конфигурациясы

Turbopack-тың чанкылеуі әдейі конфигурацияланбайтын: splitChunks баламасы жоқ, ал webpack magic comments-тері (webpackChunkName және оның серіктестері) еленбейді. Кірістірілген async-чанкылеуге қатысты жалғыз тиісті эксперименттік жалауша продакшн-билдтерде әлдеқашан әдепкі бойынша қосулы. Бұрайтын тұтқа шын мәнінде қалмады.

Тұйық №5: webpack-қа ауысу (және бәрін түсіндірген эксперимент)

Webpack конфигурацияланады, сондықтан біз сайтты next build --webpack арқылы өткіздік. Әдепкі конфиг: дәл сол мегачанк, ~1 MB, барлық секциялар. Содан кейін біз мәселені секция бойынша cacheGroup-пен мәжбүрледік — секцияның әрбір каталогына бір чанк, enforce: true:

next.config.js (webpack experiment)
config.optimization.splitChunks.cacheGroups.sections = {
  test: /[\\/]components[\\/]sections[\\/]/,
  chunks: 'all',
  minSize: 0,
  enforce: true,
  priority: 50,
  name: (module) => 'section-' + dirNameOf(module), // one chunk per section
}
// Result: the 1 MB megachunk split into 34 neat per-section files...
// ...and the page loaded ALL 34 of them. Same total bytes. Zero win.

Чанктер тамаша бөлінді — 34 ұқыпты секция бойынша файл. Және бет солардың барлық 34-ін жүктеді. Дәл сол жалпы байттар, дәл сол бөгелген гидрация, енді көбірек HTTP сұраныстарымен. Дәл осы сәтте нақты мәселе даусыз болды: біз кодтың қалай оралатынын оңтайландырып жүрдік, ал мәселе роут қандай кодқа сілтенетінінде еді.

Нақты түбегейлі себеп: чанкылеу емес, қолжетімділік

Міне механизм. Секция компоненттері — клиенттік компоненттер ('use client') — оларда хендлерлер, слайдерлер, аналитика бар. Серверлік компоненттер ағашы клиенттік компонентке сілтенгенде, бандлер сол компоненттің чанкін роуттың клиенттік бандліне қосуға тиіс, әйтпесе гидрация болмайды. CMS-басқарылатын роут қандай клиенттік компоненттерге сілтенеді? Рендерер секцияларды CMS-тен келген рантайм жолымен шешеді — сондықтан статикалық тұрғыда реестрдегі ӘРБІР секция рендерерді қолданатын әрбір беттен қолжетімді. Бандлер /pricing-тің тек 12-ін ғана рендерлейтінін біле алмайды. Ол барлық ~100-ін дайындауға тиіс.

next/dynamic көмектеспейді, өйткені ол ұсынатын жалқаулық — клиенттік: ол чанктің рендерге қатысты қашан жүктелетінін кейінге қалдырады, бірақ сервер клиент бірде-бір байт орындағанша нені рендерлейтінін әлдеқашан шешіп қойған. Сервер нені рендерлесе — сол гидрациялануға тиіс; нені рендерлеу мүмкін болса — сол жөнелтілуге немесе қолжетімді болуға тиіс. Рантаймда шешілетін реестрмен «рендерлеу мүмкін» дегеніміз «бәрі» дегенге тең.

Бандлер қолжетімді нәрсені бандлдейді. Егер сіздің резолверіңіз 100 компонентке жете алса — сіздің роутыңыз 100 компонент жөнелтеді: бір чанкке немесе отыз төртке бөлінген, бірақ бәрібір жөнелтілген. Жалғыз шынайы тұтқа — қолжетімділіктің өзін кішірейту.

Шешім: білімді build-таймға көшіру (кодген + rewrites)

CMS-басқарылатын SSG беттерінің құтқарушы қасиеті: build кезінде сервер әрбір бет қандай секцияларды қолданатынын дәл біледі — ол HTML-ді алдын ала рендерлеу үшін оларды CMS-тен алады. Білім бар; ол жай ғана рантаймда, бандлер оны көре алмайтын жерде тұтқында. Сондықтан біз оны бандлер іске қосылғанға дейін кодқа материализациялаймыз.

Кодген скрипті билдтің бірінші қадамы ретінде орындалады (пакеттің build скриптінде тізбектелген — сондықтан әрбір пайплайн оны тегін алады: жергілікті, CI, екі деплой жолы да):

scripts/generate-landing-routes.mjs
// Runs BEFORE next build (chained in the "build" script).
// 1. Find the routes that render CMS pages (scan for the renderer import).
// 2. Ask the CMS which sections each page actually uses —
//    union across every locale, and across A/B variants.
// 3. Stamp a physical page file per route whose import map contains
//    ONLY those sections. The bundler does the rest.
const pages = await fetchAllCmsPages()           // slug -> sections[]
for (const page of inScope(pages)) {
  const concepts = unionAcrossLocalesAndVariants(page)
  writeFileSync(
    `app/[lang]/(generated)/g/${page.key}/page.tsx`,
    stampTemplate({ page, concepts })            // inline import() per concept
  )
}
writeFileSync('generated-routes.manifest.json', { rewrites })
// Any failure => write nothing => the site behaves exactly as before.

Скоуптағы әрбір бет үшін ол физикалық роут файлын штамптайды, оның импорт картасында тек сол беттің секциялары болады — барлық локальдер бойынша (әртүрлі локальдердің секция жиындары әртүрлі болуы мүмкін) және A/B нұсқалары бойынша бірігу. Бандлер үшін бұл генерацияланған файл — тар статикалық қолжетімділігі бар қарапайым бастапқы код, сондықтан ол ешқандай конфигурациясыз шағын роут бойынша бандл шығарады:

app/[lang]/(generated)/g/home/page.tsx (auto-generated)
// AUTO-GENERATED on every build — the "manifest" is literal code.
const LOADERS: SectionLoaders = {
  'sections.hero': {
    concept_5: () => import('@/components/sections/Hero/HeroV5'),
  },
  'sections.faq': {
    concept_1: () => import('@/components/sections/Faq/FaqV1'),
  },
  // ...exactly the 13 section types this page renders. Nothing else.
}

export default async function GeneratedHome({ params }) {
  const { lang } = await params
  return <CmsPage loaders={LOADERS} params={{ lang, slug: 'home' }} />
}

Генерацияланған роуттар ұсқынсыз ішкі жолдың астында тұрады — қолданушылар оны ешқашан көрмейді. next.config-тегі rewrites() блогы шығарылған манифесті оқиды және нақты URL-дерді beforeFiles ішіндегі генерацияланған роуттарға сәйкестендіреді. Шешуші қасиет: егер манифест жоқ немесе бұзылған болса — rewrites жай ғана болмайды және әрбір бетке тиылмаған әмбебап роут қызмет етеді:

next.config.ts
async rewrites() {
  // Missing/broken manifest => zero rewrites => yesterday's behavior.
  // The codegen can never take the site down.
  let generated: Array<{ source: string; destination: string }> = []
  try {
    const manifest = JSON.parse(
      fs.readFileSync(path.join(__dirname, 'generated-routes.manifest.json'), 'utf8')
    )
    if (Array.isArray(manifest?.rewrites)) generated = manifest.rewrites
  } catch { /* fall back to universal routes */ }

  return {
    beforeFiles: [
      ...generated,
      // e.g. { source: '/:lang(en|de|fr|...)', destination: '/:lang/g/home' }
    ],
  }
}

Рендер тізбегі — түпнұсқаның айнасы — сол дерек алу, сол SEO, сол аналитика — бүкіл мәні болып табылатын бір айырмашылықпен: компоненттер картасы жаһандық реестрден импортталудың орнына проп ретінде келеді. Бір инвариант сақталуы керек, әйтпесе бүкіл ұтыс буланып кетеді: генерацияланған роуттың модуль графындағы ешнәрсе жаһандық реестрді импорттауға тиіс емес. Оның ішінде жанама жолдар да — біздің дерек алу көмекшіміз тек сұраныс шаблондарын оқу үшін реестрді импорттайтын, бұл барлық секцияларды қайтадан қолжетімді ететін еді; метадеректерді өзінің реестрсіз бөлек модуліне бөлуге тура келді.

SectionRendererGen.tsx
// Same rendering logic as before — but the component map arrives
// as a PROP from the generated page. Nothing in this chain may import
// the global registry, or every section becomes reachable again.
export async function SectionRendererGen({ loaders, name, concept, id }) {
  const data = await fetchSectionData(name, id)
  const loader = loaders[name]?.[concept] ?? loaders[name]?.['concept_1']
  if (!data || !loader) return null
  const { default: Component } = await loader()
  return <Component {...data} />
}

A/B тестілеуден аман өту (бәрі сұрайтын бөлік)

CMS беттері A/B эксперименттерін жүргізеді: middleware әрбір сұраныс бойынша feature флагын бағалайды және нұсқа тобындағы қолданушыларды сол URL астындағы басқа бетке рерайттейді. Рантаймда шешу дәл осы себептен бастапқыда болған еді — олай болса build-тайм жүйесі мұнымен қалай тіл табысады? Өңдеу тәртібін құрметтеу арқылы. Next.js-те middleware әрдайым beforeFiles rewrites-тан бұрын орындалады, сондықтан екі қабат бір-бірімен күресудің орнына құралады:

request /
  │
  ▼
proxy (middleware) — A/B decision, ALWAYS runs first
  ├─ no experiment        → pass through as /en
  ├─ user in CONTROL      → set cookie, pass through as /en
  └─ user in VARIANT      → rewrite to /en/ab/<variant-slug>
  │
  ▼
beforeFiles rewrites — OUR manifest, runs on whatever path proxy chose
  ├─ /en                  → /en/g/home            (generated, small)
  ├─ /en/ab/<variant>     → /en/g/ab-<variant>    (generated, small)
  └─ anything unmatched   → untouched → universal route (fallback)

Кодген эксперимент конфигтерін middleware қолданатын дәл сол көзден оқиды (әдейі жасалған инвариант — дрейф мүмкін емес) және әрбір нұсқаға генерацияланған бетті, плюс нұсқаның ішкі URL-і үшін нақты rewrite штамптайды. Бақылау қолданушылары жылдам генерацияланған бетті алады; нұсқа қолданушылары өздерінің жылдам генерацияланған бетін алады; қолданушыға көрінетін URL ешқашан өзгермейді. Соңғы билдтен кейін жасалған эксперимент келесі деплойға дейін жай ғана әмбебап роутқа құлайды — кешегі өнімділікке дейін деградацияланады, ешқашан сынбайды.

Fail-open келісімі

Бүкіл жүйе статус-квоға дейін деградациялауға арналған, ешқашан одан төмен емес. Мұның жұмыс істегенін продакшнда кездейсоқ көрдік: алғашқы деплойда CI ортасы CMS URL-ін жергілікті билдтердегіден басқа айнымалы атауымен ашты. Кодген CMS таппады, ештеңе шығармады — және сайт әрбір бетті әмбебап роут арқылы берді, билд жасыл, қолданушыларға нөл әсер. Екі жолдық бір түзетуден кейін генерацияланған роуттар пайда болды. Келісім:

  • CMS қолжетімсіз немесе бет парсталмайтын → сол бет (немесе бәрі) өткізіп жіберіледі; билд кодгеннің кесірінен ешқашан құламайды.
  • Манифест жоқ → rewrites жоқ → бәріне әмбебап роут қызмет етеді, дәл жобаға дейінгідей.
  • Билдтен кейін CMS-те секция құрылымы өзгерді → бет келесі деплойға дейін ескі жиынды рендерлейді (қайта деплойды тудыратын publish-webhook бұл терезені бірнеше минутқа қысқартады). Контент өзгерістері әсер етпейді — деректер әлі де ISR арқылы тірідей алынады.
  • Генерацияланған файлдар gitignore-де және әрбір билдте қайта генерацияланады; рецензенттер 700 штампталған файлды емес, генераторды қарайды.

Нәтижелер

Метрика (басты бет)ДейінКейін
First-load JS (шикі, бастапқы скрипт жиыны)5,127 KB (57 chunks)2,411 KB (39 chunks)
Бандлдегі секция компоненттері13716
Барлық секциялардың мегачанкі~1,200 KB0 KB

−53% first-load JavaScript, үш тәсілмен тәуелсіз расталды: бастапқы скрипт жиынын өлшеу, модуль бойынша source-map атрибуциясы (дәл беттің өзінің 16 компоненті — 13 сәйкестендірілген секция плюс олардың бала компоненттері) және тірі деплой чанктері бойынша маркерлер grep-і:

// Verify on the LIVE deployment without source maps: CSS class names
// are embedded in the JS chunks, so grep for section markers.
let all = ''
for (const src of initialScriptsOf('/en')) all += await fetchText(src)

// must be there:  the sections the page renders
console.log(all.includes('HeroV5'))          // true ✅
// must NOT be there: everything else
console.log(all.includes('fortune_wheel'))   // false ✅
console.log(all.includes('holiday_offer'))   // false ✅

Дәл сол сандар продакшн-превьюде килобайтқа дейінгі дәлдікпен қайталанды. Реттеуге тұрарлық бір күтім: сіздің DevTools желі ЖАЛПЫ саны дерлік өзгермеген болып көрінеді, өйткені load-тан кейін роутер басқа роуттардың (әлі де әмбебап) бандлдерін фонда префетчтейді. Бұл қалыпты жағдай — фондық префетч ештеңені бөгемейді. Өнімділік дегеніміз — интерактивтілікке дейін не жүктелетіні, ал дәл сол сан екі есе азайды. Егер скриншотқа ыңғайлы дәлел қажет болса, Lighthouse және оның treemap-і мұны бір мәнді көрсетеді.

Ымыралар, адал айтқанда

  • Құрылымның жаңалығы деплойға байланысты: CMS-те секцияны қосу/жою оңтайландырылған роутқа жету үшін қайта билдті қажет етеді (webhook-тудыратын деплойлар терезені бірнеше минутқа қысқартады; fallback олқылықты жабады).
  • Кодген сіздің CMS-іңізді және сіздің роут конвенцияларыңызды парстайды — бұл сіз иеленетін және қолдауыңыз керек ~400 жол код, оның ішінде реестр мен роут пропстарын парстауы да бар.
  • Масштаб: жүздеген CMS беттері → жүздеген бірегей секция комбинациялары (біздің ~700 бетімізде ~380 бірегей жиын, ұзын құйрық болды). Айқын scope тұрақтысының артындағы ең көп трафикті беттерден бастаңыз; құйрыққа fallback қызмет етеді.
  • Бұл сайттың жалпы JS-ін кішірейтпейді — нұсқаға толы кітапханалар мен бай секциялар әлі де бар. Ол әрбір роут не жөнелтетінін кішірейтеді. Келесі тұтқа (презентациялық секцияларды серверлік компоненттерге түрлендіру) — бөлек, үлкенірек жоба.

Түйіндер

  1. next/dynamic бөлу нүктелерін жасайды, кепілдіктерді емес. Сервер-басқарылатын беттерде жалқаулық импорттардың қалай жазылғанымен емес, роут неге жете алатынымен шешіледі.
  2. Чанкылеу конфигурациясы қолжетімділікті емдей алмайды. Біз мұны сарқа дәлелдедік: мегачанкті 34 секция бойынша чанкке бөлу ештеңені өзгертпеді — барлық 34-і жүктелді.
  3. Егер сіздің беттеріңіз CMS-тен SSG/ISR болса, сізге қажет ақпарат build-таймда әлдеқашан бар. Кодген — көпір: рантайм ізденістерін роут бойынша статикалық импорттарға айналдырыңыз.
  4. middleware-мен құрастырыңыз, онымен күреспеңіз: middleware ҚАЙ бетті (A/B) шешеді, rewrites сол беттің ҚАЙ ІСКЕ АСЫРЫЛУЫН шешеді. Тәртіп олардың бір-бірінің үстіне қатталуына кепілдік береді.
  5. Fail-open жүйелерін құрыңыз: манифест жоқ = кешегі сайт. Біздің алғашқы продакшн деплойымыз fallback-ты кездейсоқ сынап көрді және қолданушылар ештеңе байқамады.

Паттерн «рантаймда CMS деректерімен шешілетін компоненттер реестрі» болатын кез келгеніне жалпыланады — page builder-лер, виджет жүйелері, тема қоюға болатын дүкендер. Егер сіздің роутыңыз кез келген нәрсені рендерлей алса — ол бәрін жөнелтеді. Билдке әрбір беттің шын мәнінде не екенін білдіріңіз, сонда бандлер сіз одан әрдайым күткен нәрсені ақыры істейді.