ข้ามไปยังเนื้อหา

Turbopack และ Build Performance

Turbopack คือ bundler ของ Next.js ที่เขียนด้วย Rust ตอนนี้เป็น default ทั้ง next dev และ next build และเร่ง feedback loop ด้วยการ compile แบบ incremental แทนที่จะ rebuild ทุกอย่างใหม่

ใน Next.js ปัจจุบัน (เวอร์ชัน 16 ณ ตอนที่เขียน) Turbopack เป็น bundler default ทั้ง next dev และ next build ฝั่ง development กับ Turbopack นิ่งมาระยะหนึ่งแล้ว ส่วน path ของ production build เป็นชิ้นที่ใหม่กว่า ควรเช็กกับ docs ของเวอร์ชันที่คุณใช้จริง คุณไม่ต้องใส่ flag อะไรเพื่อเปิดใช้ — script ธรรมดาก็พอ

{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}

ถ้าเจอปัญหาแล้วต้องกลับไปใช้ bundler เดิม คุณ opt กลับไปใช้ webpack ได้เป็นราย command

{
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start"
}
}

next.config ที่มีฟังก์ชัน webpack() แบบ custom แต่ไม่มี key turbopack จะทำให้ next build แบบ default fail แทนที่จะเงียบ ๆ ข้ามไป — ใส่ --webpack เพื่อใช้ webpack ต่อ หรือ --turbopack เพื่อให้ข้าม webpack config ไป

แก่นคือ incremental compilation Turbopack สร้าง dependency graph แล้วคำนวณใหม่เฉพาะส่วนที่ได้รับผลจากการเปลี่ยนแปลง cold start จึงเร็วขึ้นและ Hot Module Replacement ยังเกือบทันทีแม้แอปจะโตขึ้น เมื่อคุณ save ไฟล์ Turbopack จะ recompile เฉพาะ module นั้นกับตัวที่ขึ้นกับ module นั้น ไม่ใช่ทั้ง route

webpack คือ JavaScript bundler ที่สุกงอมและมี plugin ecosystem มหาศาล แต่แก่น JavaScript และแนวโน้มที่จะ rebuild ทั้งก้อนกลายเป็นคอขวดในแอปใหญ่ Turbopack เขียนด้วย Rust ออกแบบมาให้ทำงาน multi-core และถือว่างาน incremental แบบละเอียดเป็น default ไม่ใช่ของเสริม Turbopack ไม่รองรับ webpack plugin เลย — รองรับแค่ loader ของ webpack ที่ทดสอบแล้วบางส่วนเท่านั้น — และคุณตั้งค่ามันใต้ key turbopack ระดับบนสุด (ชื่อเดิมคือ experimental.turbo ยังใช้ได้เป็น alias แต่ config ใหม่ควรใช้ key ระดับบนสุด)

สอง option ที่ครอบคลุม webpack resolve config ที่คนใช้จริงเกือบทุกกรณี:

  • resolveAlias — เทียบเท่า resolve.alias ของ webpack ตรง ๆ สำหรับ map import specifier ไปยัง module อื่น
  • rules — เทียบเท่า module.rules ของ webpack สำหรับใช้ loader (เช่น @svgr/webpack, yaml-loader, raw-loader) กับไฟล์ที่ตรงกับ glob
// next.config.ts — Turbopack-specific configuration
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
turbopack: {
resolveAlias: {
underscore: 'lodash', // same shape as webpack's resolve.alias
},
resolveExtensions: ['.mdx', '.tsx', '.ts', '.jsx', '.js', '.json'],
// rules: { '*.svg': { loaders: ['@svgr/webpack'], as: '*.js' } }
},
}
export default nextConfig

ช่องว่างที่ต้องระวังตอนย้าย: loader runner ของ Turbopack (loader-runner) รองรับ API ของ loader มาตรฐานส่วนใหญ่ แต่ไม่ทั้งหมด — this.importModule/this.loadModule ไม่รองรับเลย, this.emitFile ไม่รองรับเลย และ this.fs รองรับแค่ readFile เท่านั้น loader ที่พึ่งพา API พวกนี้ต้องหาทางเลือกที่เข้ากับ Turbopack ได้ ไม่ใช่แค่เพิ่ม entry ใน rules

reactCompiler: true เปิด automatic memoization ตัดงาน useMemo/useCallback ด้วยมือไปเยอะ จุดที่ docs พูดตรง ๆ คือ: compiler รันผ่าน Babel plugin — Next.js ใส่ SWC pass แบบ custom มาก่อนเพื่อจำกัดว่าไฟล์ไหนจะถูกส่งเข้า Babel บ้าง แต่คุณก็ยังจ่ายต้นทุนตอน build จริง แม้จะจำกัดขอบเขต และต้องติดตั้ง babel-plugin-react-compiler เอง

next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true, // requires: npm install -D babel-plugin-react-compiler
}
export default nextConfig

Next.js 16.3 เพิ่มทางออกจากต้นทุน Babel นั้น: experimental.turbopackRustReactCompiler รัน native Rust port ของ compiler ข้างใน Turbopack ตรง ๆ — ไม่ต้องผ่าน Babel pass บน Node.js และไม่ต้องติดตั้ง babel-plugin-react-compiler เลย มันยังต้องมี reactCompiler: true ตั้งไว้อยู่ดี (มันแค่เลือกว่า implementation ไหน จะรัน) และใช้ได้เฉพาะกับ Turbopack เท่านั้น

// next.config.ts — 16.3+: skip the Babel plugin entirely
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true,
experimental: {
turbopackRustReactCompiler: true, // native Rust port, Turbopack-only, no babel-plugin-react-compiler needed
},
}
export default nextConfig

flag ของ next build สองตัว ทั้งคู่มีจริงและอยู่ใน Next.js เวอร์ชันนี้เป๊ะ (npx next build --help):

  • --debug-prerender — “Enables debug mode for prerendering. Not for production use!” — สำหรับ trace ว่าเกิดอะไรขึ้นจริง ๆ ตอน route ถูก prerender เมื่อการแบ่ง static/dynamic ไม่ทำงานตามที่คาด
  • --profile — “Enables production profiling for React” สำหรับจับ React profiler trace ของ production build
Terminal window
next build --debug-prerender # trace prerendering decisions, dev-only diagnostic
next build --profile # React production profiling

next build --experimental-analyze ก็มีอยู่จริงเป็น flag ตอน build เทียบเท่ากับ command แยก next experimental-analyze ที่พูดถึงในบทก่อนหน้า

Turbopack เก็บงานลง disk ได้ การ start dev หรือ build ครั้งถัดไปจึงใช้ผล compile เดิมซ้ำแทนที่จะเริ่มจากศูนย์ cache ทั้งสองตัวเปิดโดย default (dev ตั้งแต่ 16.1, build ตั้งแต่ 16.3) แม้ option จะยังอยู่ใต้ experimental ก็ตาม cache ฝั่ง build อยู่ที่ .next/cache/turbopack จึงช่วยได้ก็ต่อเมื่อ CI ของคุณ restore .next/cache ระหว่าง build — เซ็ต option เป็น false เพื่อ opt out

// next.config.ts — both caches are on by default; opt out per command
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true, // default: true (16.1+)
turbopackFileSystemCacheForBuild: false, // default: true (16.3+); false if CI never restores .next/cache
},
}
export default nextConfig
flowchart LR
  Save[Save a file] --> Graph[Dependency graph]
  Graph --> Changed[Recompile changed module + dependents]
  Graph --> Cache[Reuse unchanged from cache]
  Changed --> HMR[Fast HMR / build]
  Cache --> HMR
  Compiler["reactCompiler: true"] -->|Babel plugin, default| Slow[Localized build cost]
  Compiler -->|turbopackRustReactCompiler: true, 16.3+| Fast[Native Rust, no babel-plugin-react-compiler]
incremental compilation ใช้งานที่ไม่เปลี่ยนซ้ำ

rules/resolveAlias ของ Turbopack คือชั้นแปลภาษา ไม่ใช่การเขียนใหม่ทั้งหมด — คุณกำลังบอก Turbopack ว่า “นี่คือ webpack loader ที่คุณเชื่อถืออยู่แล้ว รันมันแบบที่เคยรัน” นั่นคือเหตุผลที่ขอบเขต API ของ loader (ไม่ใช่พฤติกรรมแกนของ bundler) คือจุดที่การย้ายมักพังจริง การแยก React Compiler (Babel เทียบกับ Rust port ใต้ turbopackRustReactCompiler) ก็เป็นทางเลือกรูปแบบเดียวกับ Turbopack เทียบกับ webpack เอง: ทางหนึ่งสุกงอมใช้ได้ทุกที่แต่ต้องมี dependency เพิ่ม ทางใหม่ตัด dependency นั้นออกแต่จำกัดแค่ Turbopack เท่านั้น

ได้อะไรประโยชน์ต้นทุน
Turbopack default (next dev/next build)cold start เร็วขึ้น HMR เกือบทันทีwebpack plugin ใช้ไม่ได้เลย รองรับแค่ loader บางส่วนที่ทดสอบแล้ว
turbopack.resolveAlias/rulesเทียบเท่า resolve.alias/loader ของ webpack ตรง ๆloader ที่ใช้ importModule, emitFile หรือ this.fs ส่วนใหญ่ต้องหาวิธีอื่น
reactCompiler: true (Babel, ทาง default)automatic memoization ตัดงาน useMemo/useCallback ด้วยมือต้นทุน build จริงจาก Babel plugin ต้องติดตั้ง babel-plugin-react-compiler เอง
+ turbopackRustReactCompiler: true (16.3+)ไม่ต้องผ่าน Babel ไม่ต้องติดตั้ง dependency เพิ่มยัง experimental และใช้ได้เฉพาะ Turbopack
--debug-prerender / --profilediagnostic จริงสำหรับ prerendering และ React profiling--debug-prerender ระบุตรง ๆ ว่า “not for production use”
Turbopack FS cache (เปิด default)run dev/build ซ้ำเร็วขึ้นช่วยใน CI ได้ก็ต่อเมื่อ .next/cache ถูก restore จริงระหว่าง run
  • “Turbopack รองรับ webpack plugin เหมือนที่รองรับ loader” ไม่จริง — plugin ไม่รองรับเลย รองรับแค่ loader บางส่วนที่ทดสอบแล้วผ่าน turbopack.rules
  • “reactCompiler: true มีแค่นี้ก็พอ Next.js รันแบบ native ให้เอง” โดย default มันยังรันผ่าน Babel plugin ที่คุณต้องติดตั้งเอง (babel-plugin-react-compiler) มีแค่ turbopackRustReactCompiler: true (16.3+, Turbopack เท่านั้น) เท่านั้นที่ข้าม dependency นั้นได้
  • “--debug-prerender ปลอดภัยที่จะเปิดค้างไว้เพื่อ troubleshoot production build” help text ของ CLI เองบอกตรง ๆ ว่า “Not for production use!”
  • “Turbopack filesystem cache เร่ง CI build ทุกครั้งอัตโนมัติ” มันช่วยได้ก็ต่อเมื่อ CI ของคุณ restore .next/cache จริงระหว่าง run — ไม่งั้นทุก build ก็ยังเริ่มจากศูนย์เหมือนเดิมไม่ว่า flag จะเปิด default อยู่หรือไม่

เมื่อโค้ดที่ AI สร้างแตะ Turbopack หรือ build configuration เช็กสิ่งเหล่านี้ก่อนเชื่อ:

  • มันตั้งค่า webpack plugin ใต้ turbopack.rules หรือเปล่า? ที่นั่นรองรับแค่ loader เท่านั้น — plugin ไม่มีตัวเทียบเท่าผ่าน key นี้
  • มันเปิด reactCompiler: true โดยไม่ทั้งติดตั้ง babel-plugin-react-compiler และไม่ทั้งตั้ง turbopackRustReactCompiler: true หรือเปล่า? ต้องมีอย่างใดอย่างหนึ่ง
  • มันแนะนำ --debug-prerender สำหรับ production deployment แทนที่จะเป็นการ debug บนเครื่อง local หรือเปล่า?
  • มีฟังก์ชัน webpack() แบบ custom อยู่โดยไม่มี key turbopack แล้วสมมติว่า next build default จะเมินมันไปเฉย ๆ หรือเปล่า? ความจริงมันทำให้ build fail
  • มันสมมติว่า build cache ช่วย CI ได้โดยที่ pipeline ไม่ได้ restore .next/cache ระหว่าง run จริง ๆ หรือเปล่า?

🐛 หา bug ในโค้ดนี้

next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true,
}
export default nextConfig
เฉลย

บรรทัดนี้เปิด React Compiler แต่โดย default มันรันผ่าน Babel plugin ที่ไม่ได้ bundle มากับ Next.js — คุณต้อง npm install -D babel-plugin-react-compiler แยกต่างหาก ไม่งั้น build จะ fail เพราะหา package นั้นไม่เจอ วิธีแก้มีสองทาง: ติดตั้ง Babel plugin เอง หรือ (Next.js 16.3+, Turbopack เท่านั้น) เพิ่ม experimental: { turbopackRustReactCompiler: true } ไว้ข้าง reactCompiler: true เพื่อใช้ native Rust port แทน ซึ่งไม่ต้องมี dependency เพิ่มเลย

Turbopack เขียนด้วยภาษาอะไร
Turbopack รองรับ webpack plugin เหมือนที่รองรับ loader บางส่วนที่ทดสอบแล้วหรือเปล่า
โดย default reactCompiler: true ต้องติดตั้งอะไร และมีอะไรเปลี่ยนใน 16.3
output ของ next build --help บอกอะไรเกี่ยวกับ --debug-prerender
อะไรคือตัวเทียบเท่าแบบ Turbopack-native ของ resolve.alias ใน webpack
CI pipeline รัน next build ใหม่ทุกครั้งและไม่เคย restore .next/cache ระหว่าง run เลย turbopackFileSystemCacheForBuild ที่เปิด default เป็น true ให้อะไรกับพวกเขาจริง ๆ