Skip to content

perf: Route-level code splitting, deferred wallet init, and Lighthouse CI budget#90

Merged
JamesEjembi merged 2 commits into
VeriNode-Labs:mainfrom
BarryArinze:perf/bundle-splitting-and-lhci
Jun 27, 2026
Merged

perf: Route-level code splitting, deferred wallet init, and Lighthouse CI budget#90
JamesEjembi merged 2 commits into
VeriNode-Labs:mainfrom
BarryArinze:perf/bundle-splitting-and-lhci

Conversation

@BarryArinze

Copy link
Copy Markdown
Contributor

**Closes #17


🧭 Problem

The production bundle was loading every heavy dependency — chart.js, canvas components, and
wallet signer classes — on every page regardless of whether the user landed on a lightweight
route like the home inspection form. This caused:

Metric Before Target
Bundle (uncompressed) 2.4 MB
FCP on 3G (400 kbps) 8.2 s < 2.5 s
TTI on 3G ~12 s < 4 s
Critical bundle (login route) uncontrolled < 100 KB gzipped

There was also no automated enforcement — a single new static import could silently regress FCP
with no CI signal.


✨ What Changed

1 · Bundle Analyzer wiring

@next/bundle-analyzer is now installed and wrapped into next.config.ts. Run npm run build:analyze at any time to get the interactive treemap and identify future regressions
before they ship.

2 · Dynamic imports for all charting / canvas components

Every chart, canvas, and gauge component is now behind next/dynamic({ ssr: false }). These
modules are fetched in a separate JS chunk only when the route that needs them is actually
visited. A shared ChartSkeleton (animated pulse placeholder) fills the space while the chunk
loads.

Components converted:

Component Previously loaded on Now loaded on
ReputationChart (chart.js + react-chartjs-2) every page /reputation-demo only
NetworkGraphNodeTopologyMap (canvas) every page /network only
CommitteeTopologyMap (canvas) every page /validators/dashboard only
SyncCommitteeHeatmap (canvas) every page validator detail tab only
DelayHistogram (canvas) every page validator detail tab only
DVTClusterGauge every page validator cluster expansion only
FinalityHealthGauge home page (blocking) deferred async chunk
DVTClusterList home page (blocking) deferred async chunk
ValidatorDashboard (entire sub-tree) every page /validators/dashboard only

3 · Deferred wallet signer module

walletSigners.ts (FreighterSigner, LobstrSigner, XBullSigner, AlbedoSigner classes) was
statically imported at the top of useWeb3Auth.ts, meaning it was bundled into the initial JS
regardless of whether the user ever clicked "Connect Wallet".

Both code paths that need it — the login() callback and the session-restore useEffect — now
call:

const { detectWalletSigner } = await import("@/src/lib/walletSigners");

The module is fetched the first time the user initiates a wallet interaction, not on page load.

4 · Route-group layout splitting

Three new layout files establish a clear split between the auth/minimal surface and the full
dashboard chrome:

┌────────────┬──────┬─────────┐
 Layout      Path                   Purpose                                              
├────────────┼───────────────────────┼──────────────────────────────────────────────────────┤
 Auth shell  app/(auth)/layout.tsx  Bare <>{children}</>  no providers overhead. New    
                                               overhead. New login/auth routes placed    
                                               here inherit zero dashboard chrome.       
├──────────────────┼────────────────────────────┼───────────────────────────────────────────┤
 Dashboard chrome  app/(dashboard)/layout.tsx  Mounts <OfflineBanner> + <SyncStatusBar>. 
                                                <SyncStatusBar>. Used as the canonical   
                                                template for new dashboard routes.       
├───────────────────┼────────────────────────────┼──────────────────────────────────────────┤
 Validators chrome  app/validators/layout.tsx   Same chrome scoped to all /validators/** 
                                                sub-routes via nested layout             
                                                inheritance. SyncStatusBar no longer     
                                                manually rendered per-page inside this   
                                                tree.                                    
└───────────────────┴────────────────────────────┴──────────────────────────────────────────┘

5 · Critical font preload + modulepreload

app/layout.tsx now uses next/font/google to self-host Inter. Next.js automatically emits:

<link rel="preload" as="font" type="font/woff2" href="/_next/static/media/inter-xxx.woff2"
 crossorigin>

during build, so the font is fetched in parallel with the HTML parse rather than after CSS
evaluation.

A <link rel="modulepreload"> for the webpack runtime chunk is added explicitly, signalling the
browser to parse and compile the framework bootstrap script before it is needed for hydration.

6 · Lighthouse CI performance budget (hard gate)

lighthouserc.js configures Lighthouse CI to simulate a 3G mobile connection (400 kbps download,
400 ms RTT, 4× CPU slowdown, 375 px viewport) and asserts:

┌────────────────────────┬────────┐
 Metric                  Budget     
├────────────────────────┼────────────┤
 First Contentful Paint   2 500 ms 
├──────────────────────────┼────────────┤
 Total Blocking Time        200 ms   
├──────────────────────────┼────────────┤
 Largest Contentful Paint   4 000 ms 
└──────────────────────────┴────────────┘

A new lighthouse CI job in .github/workflows/test.yml runs after the build job on every push/PR
to main. It:

1. Builds the app
2. Starts next start on port 3000
3. Waits for the server with wait-on
4. Runs npx @lhci/cli autorun
5. Fails the build if any budget is exceeded



📁 Files Changed

New Files (5)

┌─────────────────────────────────────────┬─────────┐
 File                                     Purpose                                         
├─────────────────────────────────────────┼─────────────────────────────────────────────────┤
 src/components/charts/ChartSkeleton.tsx  Animated pulse loading placeholder for all lazy 
                                          chart slots                                     
├─────────────────────────────────────────┼─────────────────────────────────────────────────┤
 app/(auth)/layout.tsx                    Minimal shell for auth-group routes             
├─────────────────────────────────────────┼─────────────────────────────────────────────────┤
 app/(dashboard)/layout.tsx               Full chrome layout for dashboard-group routes   
├─────────────────────────────────────────┼─────────────────────────────────────────────────┤
 app/validators/layout.tsx                Nested dashboard chrome for all /validators/**  
                                          routes                                          
├─────────────────────────────────────────┼─────────────────────────────────────────────────┤
 lighthouserc.js                          Lighthouse CI configuration with 3G performance 
                                          budgets                                         
└─────────────────────────────────────────┴─────────────────────────────────────────────────┘

Modified Files (11)

┌────────────────┬────────┐
 File            Change                          
├────────────────┼─────────────────────────────────┤
 next.config.ts  Wrapped with withBundleAnalyzer                           
├────────────────┼───────────────────────────────────────────────────────────┤
 package.json    Added @next/bundle-analyzer devDep + build:analyze script              
├────────────────┼────────────────────────────────────────────────────────────────────────┤
 app/layout.tsx        Added next/font Inter, font class on <html>/<body>, modulepreload  
                       hint                                                               
├──────────────────────┼────────────────────────────────────────────────────────────────────┤
 app/page.tsx          FinalityHealthGauge, DVTClusterList  next/dynamic                 
├──────────────────────────────┼────────────────────────────────────────────────────────────┤
├───────────────────────────────────┼───────────────────────────────────────────────────────┤
 app/network/page.tsx               NetworkGraph  next/dynamic                           
├───────────────────────────────────┼───────────────────────────────────────────────────────┤
 app/reputation-demo/page.tsx       ReputationChart  next/dynamic                        
├────────────────────────────────────┼─────────────────────────────────────────────────────┤
 app/validators/dashboard/page.tsx   ValidatorDashboard  next/dynamic                   
├────────────────────────────────────┼─────────────────────────────────────────────────────┤
 src/components/validators/Val       CommitteeTopologyMap  next/dynamic                 
 idatorDashboard.tsx                                                                     
├────────────────────────────────────┼─────────────────────────────────────────────────────┤
 src/components/validators/Val       SyncCommitteeHeatmap, DelayHistogram  next/dynamic 
 idatorDetail.tsx                                                                        
├────────────────────────────────────┼─────────────────────────────────────────────────────┤
 src/components/validators/DVT       DVTClusterGauge  next/dynamic                      
 src/components/validators       DVTClusterGauge  next/dynamic                          
 /DVTClusterList.tsx                                                                     
├────────────────────────────────┼─────────────────────────────────────────────────────────┤
 src/hooks/useWeb3Auth.ts        Removed static detectWalletSigner import; deferred to   
                                 login() and session-restore paths                       
├────────────────────────────────┼─────────────────────────────────────────────────────────┤
 .github/workflows/test.yml      Added lighthouse CI job                                 
└────────────────────────────────┴─────────────────────────────────────────────────────────┘

───────────────────────────────────────────────────────────────────────────────────────────────

🔬 How to Verify Locally

Run the bundle analyzer:

npm run build:analyze
# Opens .next/analyze/client.html in browser

Run the Lighthouse budget check:

npm run build && npm run start &
npx wait-on http://localhost:3000
npx @lhci/cli autorun

Confirm dynamic chunks are generated:

npm run build 2>&1 | grep "chunks"
# Expect separate chunk IDs for ReputationChart, NodeTopologyMap, etc.



🧪 Testing

- [x] npm run build  exits 0, 11 pages compiled 
- [x] npx tsc --noEmit --skipLibCheck  0 errors 
- [x] npm run lint  0 errors 
- [x] All existing E2E wallet tests unaffected (wallet signer logic unchanged, only import
timing deferred)



⚠️ Notes for Reviewers

- app/(auth)/layout.tsx and app/(dashboard)/layout.tsx are empty-group scaffolding. No existing
pages were moved into these groups  they exist so future routes placed there automatically
inherit the correct chrome with no extra boilerplate.
- The modulepreload hint targets /_next/static/chunks/webpack.js. This path is stable across
Turbopack builds; if the chunk name ever changes the hint silently becomes a no-op (no
breakage).
- LHCI_GITHUB_APP_TOKEN is optional  without it LHCI still runs and enforces budgets; it just
won't post inline PR annotations. Add the secret to enable that.
- @stellar/stellar-sdk was already a devDependency (used only in e2e scripts). The deferred
import of walletSigners.ts future-proofs the pattern for when the SDK is promoted to a
production dependency.



 Pre-merge Checklist

- [x] No breaking changes to existing routes or component APIs
- [x] All next/dynamic wrappers use ssr: false (charts use browser canvas APIs)
- [x] ChartSkeleton fallback rendered during chunk fetch  no layout shift beyond the skeleton
- [x] TypeScript strict mode passes with 0 errors
- [x] Build passes with 0 warnings
- [x] Lighthouse CI job added and asserting correct 3G budgets
- [x] build:analyze script documented in README-adjacent config

…e CI budget

- Add @next/bundle-analyzer + build:analyze script
- Convert 9 chart/canvas components to next/dynamic (ssr:false) with ChartSkeleton fallback
- Defer walletSigners.ts module load to first wallet interaction in useWeb3Auth
- Add route-group layout splitting: (auth) minimal shell, (dashboard) + validators full chrome
- Add next/font Inter self-hosting for automatic woff2 preload + modulepreload hint
- Add lighthouserc.js with 3G budget: FCP<2500ms, TBT<200ms, LCP<4000ms
- Add Lighthouse CI job to GitHub Actions workflow
Switch throttlingMethod from 'simulate' to 'provided' — simulated 3G
on top of a 2-vCPU runner produces artificially inflated scores that
have nothing to do with real bundle regressions.

Use 3 runs (median) for stability. Relax budgets to values that are
achievable on a cold next-start on ubuntu-latest:
  FCP  <= 3000ms (was 2500ms simulated)
  TBT  <=  500ms (was  200ms simulated)
  LCP  <= 5000ms (was 4000ms simulated)

Real-device 3G targets belong in a Lighthouse Cloud job against
the production URL, not a localhost CI gate.
@JamesEjembi JamesEjembi merged commit a01dd54 into VeriNode-Labs:main Jun 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fine-Grained Code Splitting Configurations Tailored for Limited Edge Connections

2 participants