Build Optimization (apps/app)
Next.js Version & Bundler
- Next.js 16 (
^16.0.0) with Turbopack bundler (default)
- Build:
next build; Dev: Express server calls next({ dev }) which uses Turbopack by default
- React stays at
^18.2.0 — Pages Router has full React 18 support in v16
- Webpack has been fully removed (no
webpack() hook, no --webpack flag)
Turbopack Configuration
Custom Loader Rules (turbopack.rules)
| Rule |
Pattern |
Condition |
Purpose |
| superjson-ssr-loader |
*.page.ts, *.page.tsx |
{ not: 'browser' } (server-only) |
Auto-wraps getServerSideProps with SuperJSON serialization |
- Loaders are registered in
next.config.ts under turbopack.rules
condition: { not: 'browser' } restricts the loader to server-side compilation only
as: '*.ts' / as: '*.tsx' tells Turbopack to continue processing the transformed output as TypeScript
Resolve Aliases (turbopack.resolveAlias)
4 server-only packages + fs are aliased to ./src/lib/empty-module.ts in browser context:
| Package |
Reason |
fs |
Node.js built-in, not available in browser |
mongoose |
MongoDB driver, server-only |
i18next-fs-backend |
File-system i18n loader, server-only |
core-js |
Server-side polyfills |
- Uses conditional
{ browser: './src/lib/empty-module.ts' } syntax so server-side resolution is unaffected
resolveAlias requires relative paths (e.g., ./src/lib/empty-module.ts), not absolute paths — absolute paths cause "server relative imports are not implemented yet" errors
- If a new server-only package leaks into the client bundle, add it to
resolveAlias with the same pattern
SuperJSON Serialization Architecture
The next-superjson SWC plugin was replaced by a custom loader:
- Build time:
superjson-ssr-loader.ts auto-wraps getServerSideProps in .page.{ts,tsx} files with withSuperJSONProps() via Turbopack rules
- Runtime (server):
withSuperJSONProps() in src/pages/utils/superjson-ssr.ts serializes props via superjson
- Runtime (client):
_app.page.tsx calls deserializeSuperJSONProps() for centralized deserialization
- No per-page changes needed — new pages automatically get superjson serialization
- Custom serializers registered in
_app.page.tsx (ObjectId, PageRevisionWithMeta)
CSS Modules Turbopack Compatibility
:global Syntax
Turbopack only supports the function form :global(...). The block form :global { ... } is NOT supported:
// WRONG — Turbopack rejects this
.parent :global {
.child { color: red; }
}
// CORRECT — function form
.parent {
:global(.child) { color: red; }
}
Nested blocks must also use function form:
// WRONG
.parent :global {
.child {
.grandchild { }
}
}
// CORRECT
.parent {
:global(.child) {
:global(.grandchild) { }
}
}
Other Turbopack CSS Restrictions
- Standalone
:local / &:local: Not supported. Inside :global(...), properties are locally scoped by default — remove &:local wrappers
@extend with :global(): @extend .class fails when target is wrapped in :global(.class) — Sass doesn't match them as the same selector. Use shared selector groups (comma-separated selectors) instead
- IE CSS hacks:
*zoom:1, *display:inline, filter:alpha() cannot be parsed by Turbopack's CSS parser (lightningcss). Avoid CSS files containing these hacks
Vendor CSS Imports
Global CSS cannot be imported from files other than _app.page.tsx under Turbopack Pages Router. See the vendor-styles-components skill for the precompilation system that handles per-component vendor CSS.
Module Optimization Configuration
serverExternalPackages: ['handsontable'] — packages excluded from server-side bundling
optimizePackageImports — 11 @growi/* packages configured (expansion to third-party packages was tested and reverted — it increased dev module count)
Effective Module Reduction Techniques
Techniques that have proven effective for reducing module count, ordered by typical impact:
| Technique |
When to Use |
next/dynamic({ ssr: true }) |
Heavy rendering pipelines (markdown, code highlighting) that can be deferred to async chunks while preserving SSR |
next/dynamic({ ssr: false }) |
Client-only heavy components (e.g., Mermaid diagrams, interactive editors) |
| Subpath imports |
Packages with large barrel exports (e.g., date-fns/format instead of date-fns) |
| Deep ESM imports |
Packages that re-export multiple engines via barrel (e.g., react-syntax-highlighter/dist/esm/prism-async-light) |
| resolveAlias |
Server-only packages leaking into client bundle via transitive imports |
| Lightweight replacements |
Replace large libraries used for a single feature (e.g., tinykeys instead of react-hotkeys, regex instead of validator) |
Techniques That Did NOT Work
- Expanding
optimizePackageImports to third-party packages — In dev mode, this resolves individual sub-module files instead of barrel, resulting in MORE module entries. Reverted.
- Refactoring internal barrel exports — Internal barrels (
states/, features/) are small and well-scoped; refactoring had no measurable impact.
i18n HMR
I18NextHMRPlugin was removed during the Turbopack migration. Translation file changes require a manual browser refresh. The performance gain from Turbopack (faster Fast Refresh overall) outweighs the loss of i18n-specific HMR. Monitor if i18next-hmr adds Turbopack support in the future.
1---2name: build-optimization3description: GROWI apps/app Turbopack configuration, module optimization, and build measurement tooling. Auto-invoked when working in apps/app.4---56# Build Optimization (apps/app)78## Next.js Version & Bundler910- **Next.js 16** (`^16.0.0`) with **Turbopack** bundler (default)11- Build: `next build`; Dev: Express server calls `next({ dev })` which uses Turbopack by default12- React stays at `^18.2.0` — Pages Router has full React 18 support in v1613- Webpack has been fully removed (no `webpack()` hook, no `--webpack` flag)1415## Turbopack Configuration1617### Custom Loader Rules (`turbopack.rules`)1819| Rule | Pattern | Condition | Purpose |20|------|---------|-----------|---------|21| superjson-ssr-loader | `*.page.ts`, `*.page.tsx` | `{ not: 'browser' }` (server-only) | Auto-wraps `getServerSideProps` with SuperJSON serialization |2223- Loaders are registered in `next.config.ts` under `turbopack.rules`24- `condition: { not: 'browser' }` restricts the loader to server-side compilation only25- `as: '*.ts'` / `as: '*.tsx'` tells Turbopack to continue processing the transformed output as TypeScript2627### Resolve Aliases (`turbopack.resolveAlias`)28294 server-only packages + `fs` are aliased to `./src/lib/empty-module.ts` in browser context:3031| Package | Reason |32|---------|--------|33| `fs` | Node.js built-in, not available in browser |34| `mongoose` | MongoDB driver, server-only |35| `i18next-fs-backend` | File-system i18n loader, server-only |36| `core-js` | Server-side polyfills |3738- Uses conditional `{ browser: './src/lib/empty-module.ts' }` syntax so server-side resolution is unaffected39- `resolveAlias` requires **relative paths** (e.g., `./src/lib/empty-module.ts`), not absolute paths — absolute paths cause "server relative imports are not implemented yet" errors40- If a new server-only package leaks into the client bundle, add it to `resolveAlias` with the same pattern4142## SuperJSON Serialization Architecture4344The `next-superjson` SWC plugin was replaced by a custom loader:4546- **Build time**: `superjson-ssr-loader.ts` auto-wraps `getServerSideProps` in `.page.{ts,tsx}` files with `withSuperJSONProps()` via Turbopack `rules`47- **Runtime (server)**: `withSuperJSONProps()` in `src/pages/utils/superjson-ssr.ts` serializes props via superjson48- **Runtime (client)**: `_app.page.tsx` calls `deserializeSuperJSONProps()` for centralized deserialization49- **No per-page changes needed** — new pages automatically get superjson serialization50- Custom serializers registered in `_app.page.tsx` (ObjectId, PageRevisionWithMeta)5152## CSS Modules Turbopack Compatibility5354### `:global` Syntax5556Turbopack only supports the **function form** `:global(...)`. The block form `:global { ... }` is NOT supported:5758```scss59// WRONG — Turbopack rejects this60.parent :global {61 .child { color: red; }62}6364// CORRECT — function form65.parent {66 :global(.child) { color: red; }67}68```6970Nested blocks must also use function form:7172```scss73// WRONG74.parent :global {75 .child {76 .grandchild { }77 }78}7980// CORRECT81.parent {82 :global(.child) {83 :global(.grandchild) { }84 }85}86```8788### Other Turbopack CSS Restrictions8990- **Standalone `:local` / `&:local`**: Not supported. Inside `:global(...)`, properties are locally scoped by default — remove `&:local` wrappers91- **`@extend` with `:global()`**: `@extend .class` fails when target is wrapped in `:global(.class)` — Sass doesn't match them as the same selector. Use shared selector groups (comma-separated selectors) instead92- **IE CSS hacks**: `*zoom:1`, `*display:inline`, `filter:alpha()` cannot be parsed by Turbopack's CSS parser (lightningcss). Avoid CSS files containing these hacks9394### Vendor CSS Imports9596Global CSS cannot be imported from files other than `_app.page.tsx` under Turbopack Pages Router. See the `vendor-styles-components` skill for the precompilation system that handles per-component vendor CSS.9798## Module Optimization Configuration99100- `serverExternalPackages: ['handsontable']` — packages excluded from server-side bundling101- `optimizePackageImports` — 11 `@growi/*` packages configured (expansion to third-party packages was tested and reverted — it increased dev module count)102103## Effective Module Reduction Techniques104105Techniques that have proven effective for reducing module count, ordered by typical impact:106107| Technique | When to Use |108|-----------|-------------|109| `next/dynamic({ ssr: true })` | Heavy rendering pipelines (markdown, code highlighting) that can be deferred to async chunks while preserving SSR |110| `next/dynamic({ ssr: false })` | Client-only heavy components (e.g., Mermaid diagrams, interactive editors) |111| Subpath imports | Packages with large barrel exports (e.g., `date-fns/format` instead of `date-fns`) |112| Deep ESM imports | Packages that re-export multiple engines via barrel (e.g., `react-syntax-highlighter/dist/esm/prism-async-light`) |113| resolveAlias | Server-only packages leaking into client bundle via transitive imports |114| Lightweight replacements | Replace large libraries used for a single feature (e.g., `tinykeys` instead of `react-hotkeys`, regex instead of `validator`) |115116### Techniques That Did NOT Work117118- **Expanding `optimizePackageImports` to third-party packages** — In dev mode, this resolves individual sub-module files instead of barrel, resulting in MORE module entries. Reverted.119- **Refactoring internal barrel exports** — Internal barrels (`states/`, `features/`) are small and well-scoped; refactoring had no measurable impact.120121## i18n HMR122123`I18NextHMRPlugin` was removed during the Turbopack migration. Translation file changes require a manual browser refresh. The performance gain from Turbopack (faster Fast Refresh overall) outweighs the loss of i18n-specific HMR. Monitor if `i18next-hmr` adds Turbopack support in the future.