|
| 1 | +# Build Optimization Plan - Sentry Docs |
| 2 | + |
| 3 | +## Current Build Architecture Analysis |
| 4 | + |
| 5 | +### Build Flow |
| 6 | +``` |
| 7 | +1. Next.js spawns N workers (typically 8-16 based on CPU cores) |
| 8 | +2. Each worker: |
| 9 | + - Calls generateStaticParams() → getAllFilesFrontMatter() → builds doc tree |
| 10 | + - Compiles MDX pages assigned to it (getFileBySlug()) |
| 11 | + - Generates static HTML |
| 12 | +3. Main process collects results and finalizes build |
| 13 | +``` |
| 14 | + |
| 15 | +### Current Performance Metrics |
| 16 | +- **Total build time**: ~194-247 seconds |
| 17 | +- **Total pages**: 8,982 |
| 18 | +- **Doc tree builds**: 15+ times (1.2-1.7s each) = ~20-25s wasted |
| 19 | +- **MDX compilation**: 0.2-0.6s per page average |
| 20 | +- **Slow pages**: 8-10 pages taking 3-5 seconds each |
| 21 | +- **Frontmatter reading**: ~0.4-0.6s per worker |
| 22 | + |
| 23 | +### Key Bottlenecks Identified |
| 24 | +1. ❌ Doc tree rebuilt independently by each worker |
| 25 | +2. ❌ No cross-build caching of compiled MDX |
| 26 | +3. ❌ Some pages 10-20x slower than average |
| 27 | +4. ❌ Performance degrades as build progresses (0.2s → 0.6s per page) |
| 28 | +5. ❌ Frontmatter reading happens independently per worker |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## 5 Optimization Approaches |
| 33 | + |
| 34 | +### Approach 1: Pre-compute Doc Tree Before Worker Spawn |
| 35 | +**Problem**: Doc tree is built 15+ times (once per worker) during static generation |
| 36 | +**Root Cause**: Each Next.js worker calls `generateStaticParams()` or `generateMetadata()`, triggering independent doc tree builds |
| 37 | + |
| 38 | +**Solution**: |
| 39 | +- Create a build-time cache file for the doc tree |
| 40 | +- Generate it once in `next.config.ts` or a prebuild script |
| 41 | +- Load from cache in workers instead of rebuilding |
| 42 | + |
| 43 | +**Implementation**: |
| 44 | +1. Add `scripts/prebuild-doctree.ts` to serialize doc tree to `.next/cache/doctree.json` |
| 45 | +2. Modify `src/docTree.ts` to load from cache if available |
| 46 | +3. Update `package.json` build script to run prebuild step |
| 47 | + |
| 48 | +**Expected Impact**: |
| 49 | +- Save **20-25 seconds** per build |
| 50 | +- Reduce worker startup time |
| 51 | +- More consistent worker performance |
| 52 | + |
| 53 | +**Risks**: |
| 54 | +- Low risk - cache invalidation is simple (delete on clean build) |
| 55 | +- May need to handle dev vs production modes differently |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +### Approach 2: Implement Content-Based MDX Caching |
| 60 | +**Problem**: Every MDX page is compiled from scratch on every build, even if content unchanged |
| 61 | +**Root Cause**: No persistent cache of compiled MDX between builds |
| 62 | + |
| 63 | +**Solution**: |
| 64 | +- Generate hash of MDX source + dependencies (plugins, imports, etc.) |
| 65 | +- Cache compiled output keyed by hash |
| 66 | +- Store in `.next/cache/mdx/` or use Vercel build cache |
| 67 | + |
| 68 | +**Implementation**: |
| 69 | +1. Modify `src/mdx.ts` `getFileBySlug()` to check cache first |
| 70 | +2. Hash: source content + plugin config + imported files |
| 71 | +3. Store serialized compiled output |
| 72 | +4. Invalidate on hash mismatch |
| 73 | + |
| 74 | +**Expected Impact**: |
| 75 | +- **Incremental builds**: 50-80% faster (only changed pages recompile) |
| 76 | +- **Full builds**: No impact (but still valuable for preview deploys) |
| 77 | +- On typical PR: ~100-200 pages change → save **40-80 seconds** |
| 78 | + |
| 79 | +**Risks**: |
| 80 | +- Medium risk - need robust cache invalidation |
| 81 | +- Hash calculation must include all dependencies |
| 82 | +- Cache size management needed |
| 83 | + |
| 84 | +--- |
| 85 | + |
| 86 | +### Approach 3: Profile and Optimize Slow MDX Pages |
| 87 | +**Problem**: 8-10 pages take 3-5 seconds to compile (10-20x slower than average) |
| 88 | +**Root Cause**: Unknown - could be file size, complex components, or plugin overhead |
| 89 | + |
| 90 | +**Solution**: |
| 91 | +1. Add detailed profiling to slow pages |
| 92 | +2. Identify bottleneck (parsing, plugins, component resolution) |
| 93 | +3. Optimize based on findings: |
| 94 | + - Split large files |
| 95 | + - Lazy load heavy components |
| 96 | + - Skip expensive plugins for specific pages |
| 97 | + - Cache intermediate results |
| 98 | + |
| 99 | +**Implementation**: |
| 100 | +1. Add detailed timer breakpoints in `src/mdx.ts` for slow pages |
| 101 | +2. Profile: source read, bundleMDX, remark plugins, rehype plugins |
| 102 | +3. Implement specific fixes based on findings |
| 103 | + |
| 104 | +**Expected Impact**: |
| 105 | +- If we can halve slow page time (5s → 2.5s): save **20-40 seconds** |
| 106 | +- Better understanding of MDX bottlenecks |
| 107 | +- May reveal systematic issues affecting all pages |
| 108 | + |
| 109 | +**Risks**: |
| 110 | +- Low risk - purely additive profiling |
| 111 | +- May require content restructuring if files are too large |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +### Approach 4: Parallelize Frontmatter Collection |
| 116 | +**Problem**: `getAllFilesFrontMatter()` reads ~9,000 files sequentially |
| 117 | +**Root Cause**: File reading happens in a single-threaded loop |
| 118 | + |
| 119 | +**Solution**: |
| 120 | +- Use `Promise.all()` to read multiple files concurrently |
| 121 | +- Batch operations (e.g., 100 files at a time to avoid fd exhaustion) |
| 122 | +- Consider worker threads for CPU-intensive parsing |
| 123 | + |
| 124 | +**Implementation**: |
| 125 | +1. Modify `getAllFilesFrontMatter()` in `src/mdx.ts` |
| 126 | +2. Replace sequential `for` loop with batched `Promise.all()` |
| 127 | +3. Tune batch size for optimal performance (100-500 files) |
| 128 | + |
| 129 | +**Expected Impact**: |
| 130 | +- Reduce frontmatter reading from **0.4-0.6s to 0.1-0.2s** per worker |
| 131 | +- Total savings: **5-10 seconds** (across all workers) |
| 132 | +- Faster dev server startup |
| 133 | + |
| 134 | +**Risks**: |
| 135 | +- Low risk - reading files is I/O bound |
| 136 | +- Watch for file descriptor limits (handle via batching) |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +### Approach 5: Optimize MDX Plugin Chain |
| 141 | +**Problem**: Average compilation time increases from 0.2s to 0.6s as build progresses |
| 142 | +**Root Cause**: Potentially memory pressure or inefficient plugin usage |
| 143 | + |
| 144 | +**Solution**: |
| 145 | +1. Profile remark/rehype plugin execution time |
| 146 | +2. Identify expensive plugins |
| 147 | +3. Optimize: |
| 148 | + - Remove unused plugins |
| 149 | + - Replace with faster alternatives |
| 150 | + - Cache plugin results where possible |
| 151 | + - Run expensive plugins only when needed |
| 152 | + |
| 153 | +**Implementation**: |
| 154 | +1. Add timing wrapper around each plugin in `src/mdx.ts` |
| 155 | +2. Log plugin timing for representative pages |
| 156 | +3. Optimize based on findings: |
| 157 | + - Consider removing or replacing slow plugins |
| 158 | + - Add conditional plugin execution |
| 159 | + - Implement plugin-level caching |
| 160 | + |
| 161 | +**Expected Impact**: |
| 162 | +- If we reduce average time 0.4s → 0.2s: save **30-60 seconds** |
| 163 | +- More stable performance throughout build |
| 164 | +- Reduced memory pressure |
| 165 | + |
| 166 | +**Risks**: |
| 167 | +- Medium risk - may affect output if plugins are removed |
| 168 | +- Need to ensure output quality maintained |
| 169 | +- Plugin dependencies may be complex |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +## Implementation Order & Rationale |
| 174 | + |
| 175 | +### Phase 1: Quick Wins (Low Risk, High Impact) |
| 176 | +1. **Approach 1** - Pre-compute Doc Tree (save ~20-25s, low risk) |
| 177 | +2. **Approach 4** - Parallelize Frontmatter (save ~5-10s, low risk) |
| 178 | + |
| 179 | +### Phase 2: Profiling & Analysis |
| 180 | +3. **Approach 3** - Profile Slow Pages (understand bottlenecks) |
| 181 | +4. **Approach 5** - Profile Plugin Chain (understand bottlenecks) |
| 182 | + |
| 183 | +### Phase 3: Systematic Optimizations |
| 184 | +5. **Approach 2** - MDX Caching (high impact for incremental builds) |
| 185 | +6. Apply findings from profiling (Approaches 3 & 5) |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +## Success Metrics |
| 190 | + |
| 191 | +- **Target**: Reduce build time from ~240s to ~150s (37% improvement) |
| 192 | +- **Monitoring**: Track build times for each approach |
| 193 | +- **Rollback**: Each approach should be independently revertable |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## Next Steps |
| 198 | + |
| 199 | +1. ✅ Document all approaches |
| 200 | +2. ⏭️ Skip Approach 1 (Pre-compute Doc Tree) - Next.js workers don't share memory |
| 201 | +3. ✅ Implement Approach 2 (Cache Registry-Dependent Files) |
| 202 | +4. ⏳ Test and measure impact |
| 203 | +5. ⏳ Implement remaining approaches if needed |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +## ✅ IMPLEMENTED: Approach 2 - Cache Registry-Dependent Files |
| 208 | + |
| 209 | +### The Problem (from Vercel logs) |
| 210 | +- Platform-includes were NOT being cached (hundreds of "Not using cached version" messages) |
| 211 | +- Same files compiled 10-50+ times per build (e.g., `javascript.mdx`) |
| 212 | +- Individual files taking **3+ minutes** to compile |
| 213 | +- Total wasted: **~2-3 hours per build** (most of the 18 min total!) |
| 214 | + |
| 215 | +### The Solution |
| 216 | +Changed cache key strategy in `src/mdx.ts` to include: |
| 217 | +1. **File content hash** (`md5(source)`) |
| 218 | +2. **Registry data hash** (`md5(JSON.stringify({apps, packages}))`) - only for registry-dependent files |
| 219 | +3. **Branch name** (`VERCEL_GIT_COMMIT_REF`) - same for all workers + persists across commits! |
| 220 | + |
| 221 | +### Cache Key Formula |
| 222 | +```typescript |
| 223 | +// Files with registry components (<PlatformSDKPackageName>, @inject, <LambdaLayerDetail>): |
| 224 | +cacheKey = `${sourceHash}-${registryHash}-${VERCEL_GIT_COMMIT_REF}` |
| 225 | + |
| 226 | +// Regular files: |
| 227 | +cacheKey = `${sourceHash}-${VERCEL_GIT_COMMIT_REF}` |
| 228 | +``` |
| 229 | + |
| 230 | +### Why This Works (Using Branch Name!) |
| 231 | + |
| 232 | +**Within a Build (All workers share cache):** |
| 233 | +- All 8 workers get same `VERCEL_GIT_COMMIT_REF` (branch name) from Vercel |
| 234 | +- Worker 1 compiles `javascript.mdx` (3 min) → saves to `.next/cache/mdx-bundler/` |
| 235 | +- Workers 2-8 read from cache (instant) → **21 min saved per file** |
| 236 | + |
| 237 | +**Across Builds on Same Branch (HUGE WIN!):** |
| 238 | +``` |
| 239 | +Preview branch "feat/optimize-build": |
| 240 | + Commit 1: feat/optimize-build + registry_v1 → compile everything (18 min) |
| 241 | + Commit 2: feat/optimize-build + registry_v1 → ALL CACHED! (2 min) ✅ |
| 242 | + Commit 3: feat/optimize-build + registry_v1 → ALL CACHED! (2 min) ✅ |
| 243 | +``` |
| 244 | +**Every subsequent commit on the same preview branch is ~90% faster!** |
| 245 | + |
| 246 | +**When Cache Invalidates (As Expected):** |
| 247 | +- Different branch → new cache key ✓ |
| 248 | +- Registry updated → new registry hash ✓ |
| 249 | +- File content changed → new source hash ✓ |
| 250 | + |
| 251 | +**Vercel Cache Persistence:** |
| 252 | +- `.next/cache/mdx-bundler` is in `next.config.ts` line 22 `cacheDirectories` |
| 253 | +- Vercel restores this cache between deployments |
| 254 | +- Cache survives across commits as long as registry hasn't changed |
| 255 | + |
| 256 | +### Expected Impact |
| 257 | + |
| 258 | +**First Build on a Branch:** |
| 259 | +- **Platform-includes**: ~50 files × 3 min × 7 workers = **~17.5 min saved** |
| 260 | +- **Distributed-tracing**: ~30 files × 3 min × 7 workers = **~10.5 min saved** |
| 261 | +- **Total**: **~85-90% of build time** (from 18 min → ~2-3 min) |
| 262 | + |
| 263 | +**Subsequent Builds on Same Branch (Even Better!):** |
| 264 | +- **All files cached** (registry unchanged, same branch) |
| 265 | +- **Build time**: **~2 min** (just Next.js overhead) |
| 266 | +- **Per PR savings**: 3-5 commits × 16 min = **~48-80 min saved per PR!** |
| 267 | + |
| 268 | +### Files Modified |
| 269 | +- `src/mdx.ts`: Lines 687-754 (cache key generation logic) |
| 270 | +- `src/mdx.ts`: Line 887 (cache writing condition) |
| 271 | + |
| 272 | +### Testing Plan |
| 273 | +1. Clean build: `rm -rf .next && yarn build:preview` |
| 274 | +2. Check logs for: |
| 275 | + - "✓ Using cached registry-dependent file" messages |
| 276 | + - Build time reduction |
| 277 | + - No "Not using cached version" spam |
| 278 | +3. Deploy to Vercel preview and verify timing |
| 279 | + |
0 commit comments