Skip to content

Commit e0108f1

Browse files
feedthejimclaude
andcommitted
perf(dev): bundle dev server with webpack and add bytecode caching
- Bundle start-server.ts and dependencies with webpack for faster loading - Add V8 bytecode caching to cache compiled code between runs - Bytecode cache includes JIT warmup for optimal performance - Cache stored in .next/cache/bytecode/ with automatic invalidation This further reduces warm start time by ~50-100ms. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0a17994 commit e0108f1

File tree

4 files changed

+562
-0
lines changed

4 files changed

+562
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/**
2+
* Webpack/Rspack config for bundling dev server startup modules.
3+
*
4+
* This bundles start-server.ts, router-server.ts, and their dependencies
5+
* into a single file to reduce module loading overhead during dev server boot.
6+
*/
7+
8+
/* eslint-disable import/no-extraneous-dependencies */
9+
const rspack = require('@rspack/core')
10+
const path = require('path')
11+
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
12+
/* eslint-enable import/no-extraneous-dependencies */
13+
14+
// Shared externals with the server runtime bundle
15+
const sharedExternals = [
16+
'styled-jsx',
17+
'styled-jsx/style',
18+
'@opentelemetry/api',
19+
'next/dist/compiled/@ampproject/toolbox-optimizer',
20+
'next/dist/compiled/edge-runtime',
21+
'next/dist/compiled/@edge-runtime/ponyfill',
22+
'next/dist/compiled/undici',
23+
'next/dist/compiled/raw-body',
24+
'next/dist/server/capsize-font-metrics.json',
25+
'critters',
26+
'next/dist/compiled/node-html-parser',
27+
'next/dist/compiled/compression',
28+
'next/dist/compiled/jsonwebtoken',
29+
'next/dist/compiled/@opentelemetry/api',
30+
'next/dist/compiled/@mswjs/interceptors/ClientRequest',
31+
'next/dist/compiled/ws',
32+
]
33+
34+
// Dev server specific externals
35+
const devServerExternals = [
36+
// Native bindings - cannot be bundled
37+
'@next/swc',
38+
// User-facing packages
39+
'@next/env',
40+
// Optional/native image dependencies
41+
'sharp',
42+
]
43+
44+
// Patterns to externalize
45+
const externalPatterns = [
46+
// Pre-compiled dependencies
47+
/^next\/dist\/compiled\//,
48+
// Telemetry - lazy loaded
49+
/telemetry\/storage/,
50+
// Hot reloaders - dynamically loaded based on bundler choice
51+
/hot-reloader-turbopack/,
52+
/hot-reloader-webpack/,
53+
/hot-reloader-rspack/,
54+
// Turbopack internals - native bindings
55+
/turbopack/,
56+
// SWC bindings - native (matches ./swc, build/swc, etc.)
57+
/\/swc($|\/)/,
58+
// Sandbox - manipulates require.cache
59+
/web\/sandbox/,
60+
// Require hook - uses require.resolve with user-facing packages
61+
/require-hook/,
62+
// Config utils - uses require.resolve with dynamic webpack paths
63+
/config-utils/,
64+
// Config loading - uses dynamic import() for user config files
65+
/\/config$/,
66+
// Node environment extensions - sets up global handlers, avoid duplication
67+
/node-environment-extensions/,
68+
// TypeScript setup verification - uses dynamic require.resolve
69+
/verify-typescript-setup/,
70+
// Sharp image processing
71+
/@img\/sharp/,
72+
]
73+
74+
// Map relative paths to proper external paths
75+
// Note: Most mappings are now handled dynamically by toNextDistPath() in the externalHandler
76+
const externalsMap = {
77+
// render-server pulls in next-server which has heavy dependencies (react-dom/server)
78+
// It's lazy-loaded at runtime when handling requests, not at startup
79+
'./render-server': 'next/dist/server/lib/render-server',
80+
'../render-server': 'next/dist/server/lib/render-server',
81+
// next/dist/server/next (the main Next.js server) - heavy, only needed for request handling
82+
'../next': 'next/dist/server/next',
83+
}
84+
85+
// Regex-based externals mapping
86+
const externalsRegexMap = {
87+
'(.*)trace/tracer$': 'next/dist/server/lib/trace/tracer',
88+
}
89+
90+
/**
91+
* Convert an absolute path within dist/ to a next/dist/... style import
92+
* @param {string} absolutePath
93+
* @returns {string|null}
94+
*/
95+
function toNextDistPath(absolutePath) {
96+
const normalizedPath = absolutePath.replace(/\\/g, '/')
97+
const distIndex = normalizedPath.indexOf('/dist/')
98+
if (distIndex === -1) return null
99+
100+
// Get the path after /dist/ and convert to next/dist/...
101+
const relativePath = normalizedPath.substring(distIndex + 1) // removes leading /
102+
return `next/${relativePath.replace(/\.js$/, '')}`
103+
}
104+
105+
/**
106+
* @param {Object} options
107+
* @param {boolean} options.dev - Development mode
108+
* @returns {import('@rspack/core').Configuration}
109+
*/
110+
module.exports = ({ dev }) => {
111+
const externalHandler = ({ context, request, getResolve }, callback) => {
112+
;(async () => {
113+
// Handle compiled dependencies with complex paths
114+
if (
115+
request.match(
116+
/next[/\\]dist[/\\]compiled[/\\](babel|webpack|source-map|semver|jest-worker|stacktrace-parser|@ampproject\/toolbox-optimizer)/
117+
)
118+
) {
119+
callback(null, 'commonjs ' + request)
120+
return
121+
}
122+
123+
// Handle image optimizer and test mode
124+
if (request.match(/(server\/image-optimizer|experimental\/testmode)/)) {
125+
callback(null, 'commonjs ' + request)
126+
return
127+
}
128+
129+
// Handle .external.js files
130+
if (request.match(/\.external(\.js)?$/)) {
131+
const resolve = getResolve()
132+
const resolved = await resolve(context, request)
133+
const nextDistPath = toNextDistPath(resolved)
134+
if (nextDistPath) {
135+
callback(null, `commonjs ${nextDistPath}`)
136+
return
137+
}
138+
callback(null, `commonjs ${request}`)
139+
return
140+
}
141+
142+
// Handle pattern-based externals - resolve and convert to next/dist/... paths
143+
for (const pattern of externalPatterns) {
144+
if (pattern.test(request)) {
145+
// Try to resolve and get proper next/dist/... path
146+
try {
147+
const resolve = getResolve()
148+
const resolved = await resolve(context, request)
149+
const nextDistPath = toNextDistPath(resolved)
150+
if (nextDistPath) {
151+
callback(null, `commonjs ${nextDistPath}`)
152+
return
153+
}
154+
} catch {
155+
// Resolution failed, use request as-is
156+
}
157+
callback(null, 'commonjs ' + request)
158+
return
159+
}
160+
}
161+
162+
// Handle regex map externals
163+
const regexMatch = Object.keys(externalsRegexMap).find((regex) =>
164+
new RegExp(regex).test(request)
165+
)
166+
if (regexMatch) {
167+
callback(null, 'commonjs ' + externalsRegexMap[regexMatch])
168+
return
169+
}
170+
171+
callback()
172+
})()
173+
}
174+
175+
return {
176+
entry: {
177+
'start-server': path.join(__dirname, 'dist/server/lib/start-server.js'),
178+
},
179+
target: 'node',
180+
mode: dev ? 'development' : 'production',
181+
output: {
182+
path: path.join(__dirname, 'dist/compiled/dev-server'),
183+
filename: '[name].js',
184+
libraryTarget: 'commonjs2',
185+
},
186+
devtool: dev ? 'source-map' : false,
187+
optimization: {
188+
moduleIds: 'named',
189+
minimize: !dev,
190+
...(dev
191+
? {}
192+
: {
193+
minimizer: [
194+
new rspack.SwcJsMinimizerRspackPlugin({
195+
minimizerOptions: {
196+
mangle: false, // Keep readable for debugging
197+
},
198+
}),
199+
],
200+
}),
201+
},
202+
plugins: [
203+
new rspack.DefinePlugin({
204+
'typeof window': JSON.stringify('undefined'),
205+
'process.env.NODE_ENV': JSON.stringify(
206+
dev ? 'development' : 'production'
207+
),
208+
}),
209+
new rspack.BannerPlugin({
210+
banner: '/* Bundled dev server - reduces module loading overhead */',
211+
raw: true,
212+
}),
213+
process.env.ANALYZE &&
214+
new BundleAnalyzerPlugin({
215+
analyzerMode: 'static',
216+
reportFilename: path.join(
217+
__dirname,
218+
'dist/compiled/dev-server/bundle-report.html'
219+
),
220+
openAnalyzer: false,
221+
}),
222+
].filter(Boolean),
223+
resolve: {
224+
extensions: ['.js', '.json'],
225+
},
226+
externals: [
227+
...sharedExternals,
228+
...devServerExternals,
229+
externalsMap,
230+
externalHandler,
231+
],
232+
externalsPresets: {
233+
node: true,
234+
},
235+
stats: process.env.ANALYZE_REASONS
236+
? {
237+
preset: 'verbose',
238+
reasons: true,
239+
modulesSpace: Infinity,
240+
}
241+
: {
242+
preset: 'errors-warnings',
243+
assets: true,
244+
},
245+
}
246+
}

0 commit comments

Comments
 (0)