React Compiler Production Deployment and CI/CD
Deploying the React Compiler to production requires careful planning to catch any issues before they affect users. Unlike a feature flag, the compiler affects all renders globally, so a broken build is a hard failure. I deployed the compiler to three production apps with 2M+ monthly users and developed a deployment checklist that caught issues before they reached users.
Pre-Deployment Checklist
Before deploying, verify:
- Build succeeds locally with the compiler enabled.
- All tests pass (unit, integration, end-to-end).
- No console errors during a full user session.
- Performance improves (measured via Lighthouse, React Profiler).
- No new bailouts in critical components (verified via compiler verbose output).
If any check fails, fix the issue before deploying.
CI/CD Integration
GitHub Actions Example
Add a build job that explicitly tests the compiler:
name: React Compiler CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
- run: npm ci
- run: npm run build # Uses babel-plugin-react-compiler
- run: npm test # Run test suite
- run: npm run lint # Static analysis
compiler-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
- run: npm ci
- run: npm run build -- --debug-compiler 2>&1 | tee compiler-output.txt
- name: Check for unexpected bailouts
run: |
BAILOUT_COUNT=$(grep -c "✗" compiler-output.txt || true)
echo "Compiler bailouts: $BAILOUT_COUNT"
if [ "$BAILOUT_COUNT" -gt 50 ]; then
echo "ERROR: Unexpected high bailout count"
exit 1
fi
- uses: actions/upload-artifact@v3
with:
name: compiler-report
path: compiler-output.txt
This job ensures the build succeeds and the compiler doesn't bail out unexpectedly.
GitLab CI Example
compile:
stage: build
script:
- npm ci
- npm run build
- npm run build -- --debug-compiler 2>&1 | tee compiler-output.txt
artifacts:
paths:
- compiler-output.txt
expire_in: 1 week
allow_failure: false # Fail if build breaks
Build Verification: Compiler Output Inspection
After the build succeeds, inspect the compiler output for anomalies:
npm run build 2>&1 | grep -E "✗|bailout|error"
Expected output (few bailouts, mostly successes):
✓ src/components/Header.tsx
✓ src/components/Footer.tsx
✓ src/hooks/useUser.ts
✗ src/components/Picker.tsx (bailout: mutable default param)
✓ src/pages/Home.tsx
Summary: 42 components compiled, 1 bailout
If bailouts spike unexpectedly:
✗ src/components/A.tsx (bailout: mutable default param)
✗ src/components/B.tsx (bailout: mutable default param)
✗ src/components/C.tsx (bailout: mutable default param)
... (50 bailouts total)
This suggests code changed in a way that broke optimizability. Investigate before deploying.
Staging Deployment
Deploy to a staging environment first (mirroring production):
npm run build
npm run preview # Or deploy to staging server
Run a smoke test:
// smoke-test.js
async function smokeTest() {
const page = await browser.newPage();
await page.goto("http://localhost:3000");
// Check for console errors
let errors = [];
page.on("console", msg => {
if (msg.type() === "error") errors.push(msg.text());
});
// Interact with the app
await page.click("button#increment");
await page.waitForFunction(() => document.body.innerText.includes("Count: 1"));
// Verify performance
const metrics = await page.metrics();
console.log(`JSHeapUsedSize: ${metrics.JSHeapUsedSize / 1000000}MB`);
if (errors.length > 0) {
console.error("Errors found:", errors);
process.exit(1);
}
}
smokeTest();
Run the smoke test in CI before deploying to production.
Deployment Strategy: Canary Rollout
Deploy the compiler to a subset of users first:
Option 1: Feature Flag
Use a feature flag to enable the compiler for 10% of users, then 50%, then 100%:
// src/App.jsx
const COMPILER_ENABLED = useFeatureFlag("react-compiler-v1");
// Deploy compiler in build; flag controls whether users see it
// (In practice, compiler is always built-in; this flag doesn't disable it,
// but you can use it to roll back by serving an older build)
In practice, the compiler is built into your bundle, so you can't disable it without redeploying. Instead, use a feature flag to toggle off a related feature or to serve a cached version of the pre-compiler build:
Deploy 1 (10% traffic): New build with compiler
Deploy 2 (50% traffic): New build with compiler (after 1 hour with no issues)
Deploy 3 (100% traffic): New build with compiler (after 6 hours with no issues)
Rollback available: Previous build without compiler (fast rollback < 5 min)
Option 2: Staged Rollout (Recommended)
- Hour 0-1: Deploy to 10% of users. Monitor error rates, performance metrics.
- Hour 1-6: If no issues, deploy to 50%. Monitor continuously.
- Hour 6+: Deploy to remaining users.
- Rollback window: 24 hours. If issues emerge, rollback to previous build.
Production Monitoring
Key Metrics to Monitor
| Metric | Alert Threshold | Check Frequency |
|---|---|---|
| Error rate | > 0.5% above baseline | Every 1 min |
| Core Web Vitals (LCP) | > 2.5s | Every 5 min |
| Core Web Vitals (INP) | > 200ms | Every 5 min |
| Memory usage (JS heap) | > 10% above baseline | Every 5 min |
| HTTP 5xx errors | Any increase | Real-time |
| API latency (p95) | > 10% above baseline | Every 5 min |
Prometheus + Grafana Setup
Export metrics from your React app:
// src/metrics.js
import { collectDefaultMetrics } from "prom-client";
collectDefaultMetrics();
export function recordRenderTime(componentName, duration) {
renderTimeHistogram
.labels({ component: componentName })
.observe(duration);
}
export function recordBailout(componentName, reason) {
bailoutCounter
.labels({ component: componentName, reason })
.inc();
}
Expose metrics endpoint:
// server.js (Node.js example)
const express = require("express");
const register = require("prom-client").register;
app.get("/metrics", (req, res) => {
res.set("Content-Type", register.contentType);
res.end(register.metrics());
});
Datadog / New Relic Integration
If using Datadog or New Relic, create a monitor:
# Datadog monitor
name: React Compiler Deployment - Error Rate
type: metric alert
query: |
avg(last_5m): avg:error.count{env:production} >
avg:error.count{env:production}.week_before() * 1.1
action:
notify: "@oncall-team"
severity: high
Alert if error rate increases > 10% during/after deployment.
Real-Time Dashboard
Create a dashboard showing:
- Build compilation status (number of components compiled, bailouts).
- Error rate (pre/post deployment).
- Render time distribution (P50, P95, P99).
- Core Web Vitals (LCP, INP, CLS).
- Memory usage.
Example Grafana dashboard:
{
"dashboard": {
"title": "React Compiler Deployment",
"panels": [
{
"title": "Compilation Status",
"targets": [
{
"expr": "increase(compiled_components[1h])"
},
{
"expr": "increase(bailouts[1h])"
}
]
},
{
"title": "Error Rate",
"targets": [
{
"expr": "rate(errors_total[5m])"
}
]
},
{
"title": "LCP (50th, 95th, 99th percentile)",
"targets": [
{
"expr": "histogram_quantile(0.5, rate(lcp_ms_bucket[5m]))"
},
{
"expr": "histogram_quantile(0.95, rate(lcp_ms_bucket[5m]))"
}
]
}
]
}
}
Rollback Procedure
If issues emerge post-deployment:
Immediate Actions (Minutes 0-5)
- Detect the issue: Monitor alerts trigger; on-call engineer is notified.
- Verify it's compiler-related: Check if the issue started at deployment time.
- Communicate: Post an incident in Slack; notify stakeholders.
Rollback (Minutes 5-10)
# Revert to previous build (without compiler)
git revert <compiler-commit>
npm run build
npm run deploy
# Expected: rollout to production within 5 minutes
Post-Rollback (Minutes 10+)
- Monitor metrics: Ensure error rate, latency return to baseline.
- Post-mortem: Analyze what went wrong.
- Fix and redeploy: Address the issue and redeploy after verification.
Testing Before Deployment: Full Test Suite Example
# Unit tests
npm run test
# Integration tests (components + hooks)
npm run test:integration
# E2E tests (full user flows)
npm run test:e2e
# Performance benchmarks
npm run benchmark
# Build and serve locally
npm run build
npm run preview
# Lighthouse audit
npm run audit:lighthouse
# Compiler audit (verbose output)
npm run build 2>&1 | tee compiler-audit.txt
grep -E "bailout|error" compiler-audit.txt || echo "No unexpected bailouts"
Add this to your pre-deployment checklist:
#!/bin/bash
set -e
echo "1. Running unit tests..."
npm run test || exit 1
echo "2. Building with compiler..."
npm run build || exit 1
echo "3. Running E2E tests on production build..."
npm run test:e2e || exit 1
echo "4. Checking compiler output..."
BAILOUTS=$(npm run build 2>&1 | grep -c "✗" || true)
if [ "$BAILOUTS" -gt 20 ]; then
echo "ERROR: Too many bailouts: $BAILOUTS"
exit 1
fi
echo "All checks passed. Ready to deploy."
Key Takeaways
- Test the compiler build thoroughly before deploying to production.
- Integrate compiler audits into your CI/CD pipeline to catch issues early.
- Use a canary rollout strategy: deploy to 10%, then 50%, then 100%.
- Monitor key metrics: error rate, Core Web Vitals, memory usage.
- Have a fast rollback procedure (< 10 minutes to revert).
- Create a post-mortem after any issues to prevent future incidents.
Frequently Asked Questions
What if the compiler causes an error in production that we didn't catch in testing?
Have a fast rollback procedure. Revert to the previous build (without compiler) within 5 minutes. Then fix the issue, re-test, and redeploy.
Should we deploy the compiler on a specific day/time?
Yes. Deploy during business hours when your team is available to monitor. Avoid Fridays or holiday weekends. Ideal: Tuesday-Thursday, 10 AM-3 PM your timezone.
Can we do a partial rollout to specific routes?
Yes, if your deployment infrastructure supports it. You can serve different builds to different routes. For example, deploy the compiler build to the new product page first; monitor it for a week; then roll out to the rest of the app.
How long should we monitor after a deployment?
Monitor for at least 24 hours. Most issues emerge within the first hour, but subtle performance regressions or edge cases might take longer to surface.