# React + Vite Best Practices - Complete Reference **Version:** 2.0.0 **Framework:** React + Vite **Date:** March 2026 **License:** MIT ## Abstract Performance optimization guide for React applications built with Vite. Contains 23 rules across 6 categories covering build optimization, code splitting, development performance, asset handling, environment configuration, and bundle analysis. ## References - [Vite Documentation](https://vite.dev) - [React Documentation](https://react.dev) - [Rollup Documentation](https://rollupjs.org) --- # Sections This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules. --- ## 1. Build Optimization (build) **Impact:** CRITICAL **Description:** Vite build configuration for production. Manual chunk splitting, minification (OXC default, Terser for max compression), modern browser targets, sourcemap configuration, tree shaking, gzip/Brotli compression, and content-based asset hashing. ## 2. Code Splitting (split) **Impact:** CRITICAL **Description:** Route-based and component-level code splitting with React.lazy() and Suspense. Dynamic imports for heavy libraries, strategic Suspense boundary placement, and prefetch hints for anticipated navigation. ## 3. Development (dev) **Impact:** HIGH **Description:** Development server performance. Dependency pre-bundling with optimizeDeps, React Fast Refresh patterns for reliable HMR, and server configuration for HMR overlay, Docker, and proxy setups. ## 4. Asset Handling (asset) **Impact:** HIGH **Description:** Static asset optimization. Image lazy loading and responsive formats, SVG-as-React-components with SVGR, self-hosted web fonts with preloading, and correct usage of the public directory vs JavaScript imports. ## 5. Environment Config (env) **Impact:** MEDIUM **Description:** Environment variable management. The VITE_ prefix for client-side exposure, mode-specific env files (.env.production, .env.staging), and protecting sensitive data from being embedded in the client bundle. ## 6. Bundle Analysis (bundle) **Impact:** MEDIUM **Description:** Bundle size analysis and monitoring. Using rollup-plugin-visualizer to identify large dependencies and optimization opportunities. --- ## Configure Manual Chunks for Vendor Separation **Impact: CRITICAL (Optimal caching and parallel loading)** Without manual chunks, Vite bundles all vendor dependencies into a single chunk or mixes them with application code, leading to large initial downloads and poor cache efficiency. ## Incorrect ```typescript // vite.config.ts export default defineConfig({ plugins: [react()], build: { // No manual chunks configured // All code bundled together }, }) ``` **Problems:** - React, React DOM, and other vendors are bundled with application code - When you update your app, users must re-download everything - No parallel loading of separate chunks - Poor long-term caching — vendor code invalidated with every app change ## Correct ```typescript // vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { rollupOptions: { output: { manualChunks: { // Core React - rarely changes 'vendor-react': ['react', 'react-dom'], // Router - changes occasionally 'vendor-router': ['react-router-dom'], // UI library - if using one // 'vendor-ui': ['@headlessui/react', '@heroicons/react'], // State management // 'vendor-state': ['zustand', '@tanstack/react-query'], }, }, }, }, }) ``` ```typescript // vite.config.ts - Dynamic manual chunks function export default defineConfig({ build: { rollupOptions: { output: { manualChunks(id) { // Node modules go to vendor chunk if (id.includes('node_modules')) { // Split large libraries into separate chunks if (id.includes('react-dom')) { return 'vendor-react-dom' } if (id.includes('react')) { return 'vendor-react' } if (id.includes('@tanstack')) { return 'vendor-tanstack' } // Other node_modules return 'vendor' } }, }, }, }, }) ``` **Benefits:** - Vendor chunks cached separately from app code - Browser can download multiple chunks simultaneously - App changes don't invalidate vendor cache - Smaller, more targeted cache invalidation on updates > **Note:** Vite is transitioning from Rollup to Rolldown as its bundler. When Rolldown is fully integrated, `advancedChunks` will be the recommended replacement for `manualChunks`, offering more powerful and flexible chunking strategies. Keep an eye on Vite release notes for migration guidance. Reference: [Vite Build Options - rollupOptions](https://vitejs.dev/config/build-options.html#build-rollupoptions) --- ## Configure Optimal Minification Settings **Impact: CRITICAL (30-50% smaller bundles)** Configure optimal minification settings in Vite to reduce bundle size while maintaining debugging capabilities when needed. ## Incorrect ```tsx // vite.config.ts - Disabled or suboptimal minification import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { // Minification disabled minify: false, }, }); ``` ```tsx // Or using terser without configuration export default defineConfig({ plugins: [react()], build: { minify: 'terser', // No terser options configured - uses defaults }, }); ``` ```tsx // Code patterns that prevent effective minification // constants.ts export const CONFIG = { API_URL: 'https://api.example.com', TIMEOUT: 5000, RETRY_COUNT: 3, }; // component.tsx - Property access prevents minification function Component() { // These property names won't be minified return (
{user.firstName} {user.emailAddress}
); } ``` **Problems:** - Disabled minification ships bloated bundles to production - Unconfigured terser uses suboptimal defaults and is slower than OXC - String property access patterns prevent effective mangling - Console and debugger statements leak into production ## Correct ```tsx // vite.config.ts - Using OXC minification (Vite default) import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { // OXC is the default minifier — fastest option, no config needed // minify: 'oxc', // Remove console and debugger in production esbuild: { drop: ['console', 'debugger'], legalComments: 'none', }, }, }); ``` ```tsx // vite.config.ts - Terser for maximum compression (slower builds) import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { minify: 'terser', terserOptions: { compress: { drop_console: true, drop_debugger: true, inline: 2, dead_code: true, booleans_as_integers: true, passes: 2, }, mangle: { properties: { // Only mangle properties starting with underscore regex: /^_/, }, }, format: { comments: false, ascii_only: true, }, }, }, }); ``` ```tsx // Code patterns that support effective minification // Use private class fields for better mangling class UserService { #apiClient; #cache = new Map(); constructor(apiClient: ApiClient) { this.#apiClient = apiClient; } async #fetchUser(id: string) { if (this.#cache.has(id)) { return this.#cache.get(id); } const user = await this.#apiClient.get(`/users/${id}`); this.#cache.set(id, user); return user; } getUser(id: string) { return this.#fetchUser(id); } } ``` ```tsx // Environment-aware console removal // logger.ts const isDev = import.meta.env.DEV; export const logger = { log: isDev ? console.log.bind(console) : () => {}, warn: isDev ? console.warn.bind(console) : () => {}, error: console.error.bind(console), // Keep errors in production }; // Usage - logs are removed in production import { logger } from './logger'; function processData(data: Data) { logger.log('Processing:', data); // ... return result; } ``` **Benefits:** - OXC (default) provides the fastest minification with excellent compression - Terser produces 2-5% smaller bundles when every KB matters - Removing console/debugger prevents information leakage in production - Private class fields (`#`) enable better property mangling - Environment-aware logging keeps errors visible while stripping debug logs Reference: [Vite Build Options - minify](https://vitejs.dev/config/build-options.html#build-minify) --- ## Target Modern Browsers for Smaller Bundles **Impact: CRITICAL (10-15% smaller bundles)** Vite defaults to `'baseline-widely-available'`, which targets browser features that are widely available across all major browsers. Explicitly targeting older browsers includes unnecessary polyfills and transpilation, increasing bundle size. ## Incorrect ```typescript // vite.config.ts - Targeting old browsers unnecessarily export default defineConfig({ build: { target: 'es2015', // Too old, includes many polyfills }, }) ``` **Problems:** - Targeting es2015 adds polyfills for features all modern browsers support natively - Larger bundle size from unnecessary transpilation - Slower builds due to extra transformation passes ## Correct ```typescript // vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { // Default is 'baseline-widely-available' — good for most apps // Use 'esnext' for the smallest bundle if you control the browser environment target: 'esnext', // Or be specific about browser versions // target: ['es2022', 'edge88', 'firefox78', 'chrome87', 'safari14'], }, }) ``` ```typescript // vite.config.ts - With legacy browser support import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import legacy from '@vitejs/plugin-legacy' export default defineConfig({ plugins: [ react(), legacy({ targets: ['defaults', 'not IE 11'], // Modern chunks for modern browsers // Legacy chunks only loaded by old browsers }), ], build: { target: 'esnext', // Modern build }, }) ``` **Benefits:** - `esnext` produces the smallest bundles by using native browser features - `baseline-widely-available` (default) balances size with broad compatibility - The `@vitejs/plugin-legacy` plugin provides a fallback for older browsers without penalizing modern ones - Specific browser version targets give fine-grained control | Target | Use Case | |--------|----------| | `esnext` | Latest features, smallest bundle | | `baseline-widely-available` | Default — broad modern browser support | | `es2022` | Good balance, wide support | | Custom array | Specific browser versions | Reference: [Vite Build Options - target](https://vitejs.dev/config/build-options.html#build-target) --- ## Configure Source Maps for Production Debugging **Impact: CRITICAL (Better error tracking without exposing source)** Configure source maps appropriately for debugging in development and error tracking in production without exposing source code. ## Incorrect ```tsx // vite.config.ts - Source maps disabled import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { // ❌ Bad: Makes debugging production issues impossible sourcemap: false, }, }); ``` ```tsx // ❌ Bad: Exposing full source maps in production export default defineConfig({ plugins: [react()], build: { sourcemap: true, // Creates .map files served publicly }, }); ``` **Problems:** - Disabled source maps make production debugging impossible - Full source maps expose your original source code publicly - No integration with error tracking services like Sentry - Missing CSS source maps in development slows styling work ## Correct ```tsx // vite.config.ts - Environment-appropriate source map configuration import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig(({ mode }) => ({ plugins: [react()], build: { // ✅ Good: 'hidden' for production, full maps for staging sourcemap: mode === 'production' ? 'hidden' : true, rollupOptions: { output: { sourcemapExcludeSources: mode === 'production', }, }, }, css: { devSourcemap: true, }, })); ``` ```tsx // vite.config.ts - Integration with Sentry plugin import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { sentryVitePlugin } from '@sentry/vite-plugin'; export default defineConfig(({ mode }) => ({ plugins: [ react(), mode === 'production' && sentryVitePlugin({ org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, authToken: process.env.SENTRY_AUTH_TOKEN, release: { name: process.env.RELEASE_VERSION, }, sourcemaps: { assets: './dist/**', filesToDeleteAfterUpload: './dist/**/*.map', }, }), ].filter(Boolean), build: { sourcemap: true, // Required for Sentry plugin }, })); ``` ```nginx # nginx.conf - Block access to source maps server { listen 80; root /var/www/app/dist; location ~* \.map$ { allow 10.0.0.0/8; allow 192.168.0.0/16; deny all; } } ``` **Benefits:** - Hidden source maps enable error tracking without exposing source code - Sentry integration provides detailed production error reports with original file names - CSS source maps in development speed up styling work - Server-level blocking adds a second layer of source map protection | Option | Description | Use Case | |--------|-------------|----------| | `false` | No source maps | Not recommended | | `true` | Generates and links .map files | Development/Staging | | `'inline'` | Embeds maps in bundles | Development only | | `'hidden'` | Generates .map files without link | Production | Reference: [Vite Build Options - sourcemap](https://vitejs.dev/config/build-options.html#build-sourcemap) --- ## Configure Build for Effective Tree Shaking **Impact: CRITICAL (15-30% smaller bundles)** Configure your Vite build to effectively eliminate dead code through tree shaking, reducing bundle size significantly. ## Incorrect ```tsx // ❌ Bad: Barrel export that prevents tree shaking // utils/index.ts export * from './strings'; export * from './numbers'; export * from './dates'; export * from './arrays'; export * from './objects'; // Using namespace imports import * as utils from './utils'; function Component() { // Only using one function but importing everything return
{utils.formatDate(new Date())}
; } ``` ```tsx // ❌ Bad: Importing entire libraries import _ from 'lodash'; import moment from 'moment'; function processData(items: Item[]) { return _.uniqBy(items, 'id').map(item => ({ ...item, date: moment(item.date).format('YYYY-MM-DD'), })); } ``` ```json // ❌ Bad: package.json missing sideEffects field { "name": "my-app", "version": "1.0.0", "main": "dist/index.js", "module": "dist/index.esm.js" } ``` **Problems:** - Barrel exports with `export *` pull in entire modules even when only one function is used - Namespace imports (`import *`) prevent the bundler from identifying unused exports - Libraries like `lodash` (CJS) and `moment` are not tree-shakeable - Missing `sideEffects` field forces the bundler to assume all modules have side effects ## Correct ```tsx // ✅ Good: Named exports for better tree shaking // utils/index.ts export { formatString, capitalize, truncate } from './strings'; export { formatNumber, clamp, round } from './numbers'; export { formatDate, parseDate, isValidDate } from './dates'; export { unique, groupBy, sortBy } from './arrays'; export { pick, omit, merge } from './objects'; // Direct named imports import { formatDate } from './utils'; function Component() { return
{formatDate(new Date())}
; } ``` ```tsx // ✅ Good: Import only what you need from tree-shakeable libraries import uniqBy from 'lodash-es/uniqBy'; import { format } from 'date-fns'; function processData(items: Item[]) { return uniqBy(items, 'id').map(item => ({ ...item, date: format(new Date(item.date), 'yyyy-MM-dd'), })); } ``` ```json // ✅ Good: package.json with proper sideEffects configuration { "name": "my-app", "version": "1.0.0", "main": "dist/index.js", "module": "dist/index.esm.js", "sideEffects": [ "*.css", "*.scss", "./src/polyfills.ts" ] } ``` ```tsx // ✅ Good: vite.config.ts - Optimize dependencies for tree shaking import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { rollupOptions: { treeshake: { moduleSideEffects: 'no-external', propertyReadSideEffects: false, tryCatchDeoptimization: false, }, }, }, optimizeDeps: { include: ['lodash-es'], }, }); ``` **Benefits:** - Named exports let the bundler eliminate unused functions at build time - ESM-compatible libraries (`lodash-es`, `date-fns`) enable per-function tree shaking - The `sideEffects` field tells the bundler which files are safe to remove when unused - Aggressive treeshake options maximize dead code elimination - Use `rollup-plugin-visualizer` to audit bundle contents and verify tree shaking effectiveness Reference: [Vite Build Options - rollupOptions](https://vitejs.dev/config/build-options.html#build-rollupoptions) | [Rollup Tree Shaking](https://rollupjs.org/configuration-options/#treeshake) --- ## Configure Build-Time Compression **Impact: CRITICAL (60-80% smaller asset size)** Configure build-time compression to serve pre-compressed assets, reducing server load and improving delivery speed. ## Incorrect ```tsx // ❌ Bad: No compression configured import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { // Relying only on server-side compression // which adds CPU overhead on every request }, }); ``` ```tsx // ❌ Bad: Runtime compression adds latency import express from 'express'; import compression from 'compression'; const app = express(); // Compresses every response on-the-fly app.use(compression()); app.use(express.static('dist')); ``` **Problems:** - Server-side runtime compression adds CPU overhead and latency to every request - Lower compression levels used at runtime to keep latency acceptable - No Brotli support in most runtime compression middleware - Compression work repeated for every request instead of done once at build time ## Correct ```tsx // ✅ Good: Pre-compress assets during build import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import viteCompression from 'vite-plugin-compression'; export default defineConfig({ plugins: [ react(), // Generate gzip compressed files viteCompression({ algorithm: 'gzip', ext: '.gz', threshold: 1024, // Only compress files > 1KB deleteOriginFile: false, }), // Also generate Brotli compressed files for modern browsers viteCompression({ algorithm: 'brotliCompress', ext: '.br', threshold: 1024, }), ], build: { cssMinify: true, }, }); ``` ```tsx // ✅ Good: Advanced compression with maximum quality import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import viteCompression from 'vite-plugin-compression'; import { constants as zlibConstants } from 'zlib'; export default defineConfig({ plugins: [ react(), viteCompression({ algorithm: 'gzip', ext: '.gz', threshold: 1024, compressionOptions: { level: 9, // Maximum compression }, filter: /\.(js|css|html|json|svg|txt|xml|wasm)$/i, }), viteCompression({ algorithm: 'brotliCompress', ext: '.br', threshold: 1024, compressionOptions: { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11, // Maximum quality }, }, filter: /\.(js|css|html|json|svg|txt|xml|wasm)$/i, }), ], }); ``` ```nginx # nginx.conf - Serve pre-compressed files server { listen 80; root /var/www/app/dist; gzip_static on; brotli_static on; location ~* \.(js|css|html|json|svg|txt|xml|wasm)$ { gzip_static on; brotli_static on; try_files $uri $uri/ =404; add_header Cache-Control "public, max-age=31536000, immutable"; add_header Vary "Accept-Encoding"; } } ``` ```tsx // ✅ Good: Express server with pre-compressed file serving import express from 'express'; import expressStaticGzip from 'express-static-gzip'; const app = express(); app.use('/', expressStaticGzip('dist', { enableBrotli: true, orderPreference: ['br', 'gzip'], serveStatic: { maxAge: '1y', immutable: true, }, })); app.listen(3000); ``` **Benefits:** - Pre-compressed files eliminate on-the-fly compression overhead - Maximum compression levels achievable without impacting response latency - Brotli offers 15-25% better compression than gzip for text-based content - Faster Time to First Byte with no compression overhead per request - Both gzip and Brotli versions provide maximum browser compatibility | Format | Browser Support | Typical Ratio | Best For | |--------|-----------------|---------------|----------| | Gzip | 95%+ | 70-80% | Universal fallback | | Brotli | 90%+ | 80-90% | Modern browsers | Reference: [vite-plugin-compression](https://github.com/vbenjs/vite-plugin-compression) --- ## Configure Asset Hashing for Cache Busting **Impact: CRITICAL (Ensures latest version delivery)** Configure content-based asset hashing to enable aggressive caching while ensuring users always receive the latest version after deployments. ## Incorrect ```tsx // ❌ Bad: No hash - files get cached indefinitely import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { rollupOptions: { output: { entryFileNames: 'assets/[name].js', chunkFileNames: 'assets/[name].js', assetFileNames: 'assets/[name].[ext]', }, }, }, }); ``` ```tsx // ❌ Bad: Version-based hashing - all files invalidated on any change output: { entryFileNames: `assets/[name].${packageJson.version}.js`, chunkFileNames: `assets/[name].${packageJson.version}.js`, assetFileNames: `assets/[name].${packageJson.version}.[ext]`, } ``` **Problems:** - Without hashes, users see stale content after deployments - Version-based hashes invalidate all files even when only one changed - No way to set aggressive cache headers without risking stale content - CDNs and browser caches serve outdated files ## Correct ```tsx // ✅ Good: Content-based hashing with organized asset directories import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { rollupOptions: { output: { entryFileNames: 'assets/js/[name]-[hash].js', chunkFileNames: 'assets/js/[name]-[hash].js', assetFileNames: (assetInfo) => { const info = assetInfo.name?.split('.') || []; const ext = info[info.length - 1]; if (/png|jpe?g|gif|svg|webp|avif|ico/i.test(ext)) { return 'assets/images/[name]-[hash][extname]'; } if (/woff2?|eot|ttf|otf/i.test(ext)) { return 'assets/fonts/[name]-[hash][extname]'; } if (/css/i.test(ext)) { return 'assets/css/[name]-[hash][extname]'; } return 'assets/[name]-[hash][extname]'; }, }, }, }, }); ``` ```tsx // ✅ Good: Server caching configuration import express from 'express'; import path from 'path'; const app = express(); // Immutable caching for hashed assets (1 year) app.use('/assets', express.static(path.join(__dirname, 'dist/assets'), { maxAge: '1y', immutable: true, })); // Short cache for index.html (always check for updates) app.use(express.static(path.join(__dirname, 'dist'), { maxAge: '5m', setHeaders: (res, path) => { if (path.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache, must-revalidate'); } }, })); ``` ```nginx # nginx.conf - Optimal caching strategy server { listen 80; root /var/www/app/dist; location ~* \.html$ { add_header Cache-Control "no-cache, must-revalidate"; add_header Vary "Accept-Encoding"; try_files $uri /index.html; } location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; add_header Vary "Accept-Encoding"; try_files $uri =404; } location = /sw.js { add_header Cache-Control "no-cache, must-revalidate"; try_files $uri =404; } } ``` **Benefits:** - Content hashes create new URLs when files change, bypassing cached versions automatically - Hashed files can be cached indefinitely with the `immutable` directive - Users receive new code immediately after deployment without clearing cache - Unchanged files remain cached while only updated files are downloaded - Works seamlessly with CDNs and edge caching strategies | Cache-Control | Target | |--------------|--------| | `public, max-age=31536000, immutable` | Hashed assets | | `no-cache, must-revalidate` | HTML files, service workers | Reference: [Vite Build Options - rollupOptions](https://vitejs.dev/config/build-options.html#build-rollupoptions) --- ## Use React.lazy() for Route-Based Splitting **Impact: CRITICAL (50-80% smaller initial bundle)** Loading all route components upfront delays initial page load. Users download code for pages they may never visit. Route-based code splitting ensures users only download code for the current route. ## Incorrect ```typescript // ❌ Bad: All imports are eager - loaded immediately import { BrowserRouter, Routes, Route } from 'react-router-dom' import Home from './pages/Home' import Dashboard from './pages/Dashboard' import Settings from './pages/Settings' import Profile from './pages/Profile' import Admin from './pages/Admin' function App() { return ( } /> } /> } /> } /> } /> ) } ``` **Problems:** - All 5 page components are bundled together and loaded on initial page load - Users download code for pages they may never visit - Larger initial bundle means slower Time to Interactive - No benefit from caching individual route chunks ## Correct ```typescript // ✅ Good: Lazy load route components import { lazy, Suspense } from 'react' import { BrowserRouter, Routes, Route } from 'react-router-dom' const Home = lazy(() => import('./pages/Home')) const Dashboard = lazy(() => import('./pages/Dashboard')) const Settings = lazy(() => import('./pages/Settings')) const Profile = lazy(() => import('./pages/Profile')) const Admin = lazy(() => import('./pages/Admin')) function PageLoader() { return (
) } function App() { return ( }> } /> } /> } /> } /> } /> ) } ``` ```typescript // ✅ Good: Preload on hover for instant navigation const Dashboard = lazy(() => import('./pages/Dashboard')) function NavLink() { const preloadDashboard = () => { import('./pages/Dashboard') } return ( Dashboard ) } ``` **Benefits:** - Initial bundle reduced by 50-80% since only the current route is loaded - Time to Interactive significantly improved - Each route loads only when navigated to - Vite automatically names chunks based on file path — no magic comments needed - Preloading on hover makes navigation feel instant Reference: [React lazy](https://react.dev/reference/react/lazy) | [Vite Code Splitting](https://vitejs.dev/guide/build.html#chunking-strategy) --- ## Strategic Suspense Boundaries for Lazy Loading **Impact: CRITICAL (Progressive loading, better UX)** Without proper Suspense boundaries, a single lazy component can block the entire UI. Strategic placement of Suspense boundaries allows parts of the UI to load independently. ## Incorrect ```typescript // ❌ Bad: Single Suspense at root - entire app shows loading state function App() { return ( }>