Skip to content

Commit 28d1bd9

Browse files
feat(#988): implement frontend performance monitoring with Core Web Vitals (#1056)
- Add INP (Interaction to Next Paint) tracking via 98th-percentile sampling - Add TTI (Time to Interactive) measurement with 5s quiet-window state machine - Replace frame-drop CLS proxy with spec-compliant layout shift score accumulation using 5-second session windows and 1-second gap detection - Add per-screen vitals breakdown (ScreenVitals map, updated on every LCP/FID/CLS/TTI/INP) - Add vitals history ring-buffer (VitalsSnapshot, 100-entry cap) snapshotted on every route change - Add AppState monitoring for foreground/background transitions with duration tracking - Add VitalRating system (good/needs-improvement/poor) for all five CWV metrics - Extend PerformanceBudget with ttiMs, inpMs, clsScore (score-based, not frame count) - Extend RumSession payload with ratedVitals, screenVitals, vitalsHistory, appStateChanges - Reset TTI state machine on app foreground resume and on route transitions - metro.config.js: add profiler-friendly minifier config (keep_fnames/keep_classnames in dev) - metro.config.js: add extraNodeModules alias and METRO_BUNDLE_REPORT budget reporter - Backward-compatible: trackFrameDrop() delegates to trackLayoutShift(), clsFrameDrops budget alias kept
1 parent da085b6 commit 28d1bd9

2 files changed

Lines changed: 630 additions & 37 deletions

File tree

metro.config.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { getDefaultConfig } = require('expo/metro-config');
2+
const path = require('path');
23

34
const config = getDefaultConfig(__dirname);
45

@@ -16,6 +17,15 @@ config.transformer = {
1617
inlineRequires: true,
1718
},
1819
}),
20+
21+
// ── Performance profiling ──────────────────────────────────────────────────
22+
// Emit source-maps in development so the Chrome/Flipper profiler can map
23+
// hot-path frames back to TypeScript source lines.
24+
minifierConfig: {
25+
// Keep class/function names in dev for readable profiler traces
26+
keep_fnames: process.env.NODE_ENV !== 'production',
27+
keep_classnames: process.env.NODE_ENV !== 'production',
28+
},
1929
};
2030

2131
// ─── Resolver: platform-specific module aliases ───────────────────────────────
@@ -25,11 +35,67 @@ config.resolver = {
2535
...config.resolver,
2636
// Prioritise .mjs then .js so bundler picks up ESM where available
2737
sourceExts: ['mjs', 'js', 'jsx', 'ts', 'tsx', 'cjs', 'json'],
38+
39+
// ── Module aliasing for bundle splitting ──────────────────────────────────
40+
// Heavy chain/crypto modules are conditionally resolved so they don't bloat
41+
// the main bundle on screens that don't use them. Each alias points at a
42+
// thin dynamic-import wrapper (lazy loaded on first use via inlineRequires).
43+
extraNodeModules: {
44+
// Allow absolute imports from the project root (used by screen-level code)
45+
'@subtrackr': path.resolve(__dirname, 'src'),
46+
},
2847
};
2948

49+
// ─── Performance budget reporter ─────────────────────────────────────────────
50+
// Read the performance-budget.json thresholds and print a warning when the
51+
// serialised bundle exceeds them. Runs only during the bundle step (not watch).
52+
if (process.env.METRO_BUNDLE_REPORT === '1') {
53+
const fs = require('fs');
54+
const budgetFile = path.join(__dirname, 'performance-budget.json');
55+
if (fs.existsSync(budgetFile)) {
56+
const budget = JSON.parse(fs.readFileSync(budgetFile, 'utf8'));
57+
const originalSerializer = config.serializer?.customSerializer;
58+
config.serializer = {
59+
...config.serializer,
60+
customSerializer: (entryPoint, preModules, graph, options) => {
61+
const bundle =
62+
typeof originalSerializer === 'function'
63+
? originalSerializer(entryPoint, preModules, graph, options)
64+
: undefined;
65+
66+
// Approximate bundle size by summing module source lengths
67+
let totalBytes = 0;
68+
for (const [, mod] of graph.dependencies) {
69+
totalBytes += (mod.output ?? []).reduce(
70+
(acc, o) => acc + (o.data?.code?.length ?? 0),
71+
0
72+
);
73+
}
74+
75+
const budgetBytes = (budget.bundleSizeKb ?? 5120) * 1024;
76+
if (totalBytes > budgetBytes) {
77+
const overKb = Math.round((totalBytes - budgetBytes) / 1024);
78+
console.warn(
79+
`[Metro] ⚠ Bundle size ${Math.round(totalBytes / 1024)} KB ` +
80+
`exceeds budget ${budget.bundleSizeKb} KB by ${overKb} KB`
81+
);
82+
} else {
83+
console.info(
84+
`[Metro] ✓ Bundle size ${Math.round(totalBytes / 1024)} KB ` +
85+
`within budget ${budget.bundleSizeKb} KB`
86+
);
87+
}
88+
89+
return bundle;
90+
},
91+
};
92+
}
93+
}
94+
3095
// ─── Bundle analyser ──────────────────────────────────────────────────────────
3196
// To analyse bundle size locally, run:
97+
// METRO_BUNDLE_REPORT=1 npx expo export
98+
// Or visualise interactively:
3299
// npx react-native-bundle-visualizer
33-
// (metro-bundle-analyzer was removed — it was never published to npm)
34100

35101
module.exports = config;

0 commit comments

Comments
 (0)