Skip to content

Performance Refactor 20260402

Paulo Gomes da Cruz Junior edited this page Sep 11, 2026 · 3 revisions

Frontend Performance Refactor — Bundle Splitting, Font & CSS Optimization

Date: April 2–8, 2026 Scope: Production JS/CSS bundle size and Lighthouse mobile performance (FCP, LCP, Speed Index) Impact: JS reduced from 5.8 MB → 1.0 MB (−83%); CSS from 884 KB → 567 KB (−32%); Lighthouse mobile score from 58 to the improvements described below.


Part 1: Bundle Splitting & Tree-Shaking

Summary

This refactor targets the production JavaScript bundle size, which was a single 5.8 MB file (1.6 MB gzipped). Through manual chunk splitting, eliminating wildcard imports, and fixing barrel import patterns, the total JS payload was reduced to ~1.0 MB across multiple independently cacheable chunks (~340 KB gzipped) — an 82% reduction.

No lazy loading was introduced. All routes remain eagerly loaded to preserve compatibility with future PWA offline mode support.


Problem Statement

Lighthouse flagged poor performance scores for the application. The root cause was a single monolithic JavaScript bundle (index-*.js) containing all vendor libraries, application code, and — critically — the entire Carbon Design System icon set (~10,000 icons).

Before (baseline)

Asset Size Gzipped
index-*.js 5,820 kB 1,609 kB
index-*.css 905 kB 97 kB
workbox-*.js 6 kB 2 kB
Total JS 5,826 kB 1,611 kB
PWA precache 6,574 kB —

Changes Made

1. Manual Chunk Splitting (vite.config.ts)

File: vite.config.ts

Added a function-based manualChunks configuration to Rollup via Vite's build options. This splits the vendor code into separate, independently cacheable chunks.

manualChunks(id) {
  if (id.includes('node_modules/react/') || id.includes('node_modules/react-dom/'))
    return 'react-vendor';
  if (id.includes('node_modules/@carbon/charts'))
    return 'carbon-charts';
  if (id.includes('node_modules/@carbon/icons-react/') || id.includes('node_modules/@carbon/pictograms-react/'))
    return 'carbon-icons';
  if (id.includes('node_modules/@carbon/'))
    return 'carbon';
  // ... aws, query, router, utils
}
Why function-based, not object-based?

The simpler object syntax ({ 'react-vendor': ['react', 'react-dom'] }) matches on package entry points only. Sub-path imports like react-dom/client or aws-amplify/auth/cognito are not matched by the array form, causing modules to fall through into the wrong chunk or the main bundle. The function form matches on the resolved file path in node_modules/, catching all sub-path imports correctly.

Chunk breakdown
Chunk Contents Rationale
react-vendor React, ReactDOM Core runtime — changes rarely, caches well
carbon @carbon/react, @carbon/icons-react, @carbon/pictograms-react UI framework + icons — kept together to avoid circular dependency issues (icons depend on React internals shared with components)
carbon-charts @carbon/charts, @carbon/charts-react Charting library — only loaded when charts are used
aws aws-amplify and all sub-paths Auth SDK — updates on its own cadence
query @tanstack/react-query Data fetching — lightweight, stable
router react-router-dom Routing — stable
utils axios, lodash, luxon, idb General utilities — grouped for simplicity

2. Eliminated Wildcard Icon Imports (Primary Size Win)

Files changed:

  • src/components/core/DistrictSelection/constants.ts
  • src/components/core/DistrictSelection/DistrictItem/index.tsx
  • src/components/core/EmptySection/index.tsx
  • src/pages/RoleError/index.tsx
  • src/components/Form/TableResource/index.tsx
  • src/components/core/EmptySection/index.unit.test.tsx
The problem

Three source files used namespace imports (import * as Icons from '@carbon/icons-react') which forces the bundler to include every export from the module. For @carbon/icons-react, that means all ~10,000 icon components (~4.8 MB minified). Similarly, import * as Pictograms from '@carbon/pictograms-react' pulled in all pictograms.

The app actually uses only ~15 icons and 3 pictograms.

What was done

DistrictSelection/constants.ts — Previously stored icon names as strings and resolved them at runtime via Icons[iconName]. Refactored to store actual component references:

// Before
import * as ICON from '@carbon/icons-react';
type ClientIconType = keyof typeof ICON;
export const ClientTypeIconMap: Record<string, ClientIconType> = {
  A: 'Partnership',
  B: 'EventsAlt',
  // ... string lookups requiring the full icon set
};

// After
import { Partnership, EventsAlt, Building, /* ...only the 12 used */ } from '@carbon/icons-react';
import type { ComponentType } from 'react';
export const ClientTypeIconMap: Record<string, ComponentType> = {
  A: Partnership,
  B: EventsAlt,
  // ... direct component references, tree-shakeable
};

DistrictItem/index.tsx — Simplified icon resolution to use the map values directly instead of indexing into the namespace object.

EmptySection/index.tsx — Changed the component API from string-based icon/pictogram names to component props:

// Before — requires the entire icon/pictogram set at runtime
interface EmptySectionProps {
  icon?: keyof typeof Icons;       // string like "ErrorFilled"
  pictogram?: keyof typeof Pictograms;  // string like "Summit"
}

// After — consumers pass the actual component, tree-shakeable
interface EmptySectionProps {
  icon?: ComponentType<{ className?: string; 'data-testid'?: string }>;
  pictogram?: ComponentType<{ className?: string; 'data-testid'?: string }>;
}

All callers (RoleError, TableResource, tests) were updated to import and pass the specific icon/pictogram components they need.

Impact
Before After
carbon-icons: 4,795 kB carbon-icons: 41 kB
99.1% reduction in icon chunk size

3. Fixed Lodash Barrel Imports

Files changed:

  • src/context/preference/PreferenceProvider.tsx
  • src/context/auth/AuthProvider.tsx
  • src/hooks/useSyncPreferencesToFilters/index.tsx
The problem

Lodash's main entry point (lodash) is a CommonJS module. When you import { isEqual } from 'lodash', the bundler cannot tree-shake CJS — it includes the entire ~70 KB library even though only isEqual (~4 KB) is needed.

What was done
// Before
import { isEqual, mergeWith } from 'lodash';

// After
import isEqual from 'lodash/isEqual';
import mergeWith from 'lodash/mergeWith';

Each lodash function ships as an independent module under lodash/<functionName>. Deep imports bypass the CJS barrel entirely.

Impact

utils chunk: 181 kB → 132 kB (~49 kB saved, ~27% reduction in that chunk).


4. Type-Only Re-exports (services/types.ts)

File changed: src/services/types.ts

// Before
export * from './pagination.types';
export * from './search.types';
export * from './forestclient.types';

// After
export type * from './pagination.types';
export type * from './search.types';
export * from './forestclient.types';  // keeps runtime export (YesNoEnum)

pagination.types.ts and search.types.ts contain only TypeScript types/interfaces. Using export type * tells the compiler these are purely type-level — they're erased at compile time and cannot contribute runtime side effects. forestclient.types.ts exports YesNoEnum (a runtime value), so it keeps the regular export *.


5. Caddyfile — Asset Caching Strategy

File changed: Caddyfile

The problem

The original Caddyfile applied Cache-Control: no-store, no-cache to all responses, including hashed assets (/assets/index-CvtA56iK.js). This forced the browser to re-download every file on every page load, negating the benefits of content-hashed filenames.

What was done

Split the caching into two path-based rules:

# Hashed assets (JS/CSS/fonts/images) — cache aggressively
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"

# Everything else (HTML, config, service worker) — never cache
@notassets {
    not path /assets/*
}
header @notassets Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"

Why this is safe: Vite includes a content hash in every asset filename (e.g., react-vendor-COMZwu8M.js). When the content changes, the hash changes, producing a new URL. The immutable directive tells the browser to never revalidate — the content at that URL will never change.

Files that must not be cached (HTML, data/config.js, service-worker.js) are served outside /assets/ and retain the no-cache policy.

Impact on repeat visits

On subsequent visits, the browser serves all vendor chunks from disk cache (0 ms), only fetching the small index.html and any chunks whose content actually changed.


CSP (Content-Security-Policy) Compatibility

Why no CSP changes are needed for chunk splitting

The CSP header uses script-src 'self', which permits all scripts served from the same origin. Since all split chunks are static .js files served from the same domain under /assets/, they're automatically covered. No hashes, nonces, or additional directives are required.

When SHA-256 hashes ARE needed

SHA-256 hashes in CSP ('sha256-...') are needed when your HTML contains inline scripts — i.e., <script> tags with code directly in the HTML, not loaded from a file. In this app:

  • index.html loads scripts via <script src="/data/config.js"> and <script src="/data/params.js"> — these are external scripts (loaded by URL), not inline. They're covered by 'self'.

If your other apps use hashes like 'sha256-78ZXaz...', they likely have inline <script> blocks that need to be whitelisted individually. This app doesn't, so hashes are not required.

Summary

Script type CSP coverage
Vite-built chunks (/assets/*.js) Covered by 'self'
External config (/data/config.js, /data/params.js) Covered by 'self'
Backend URL scripts Covered by {$BACKEND_URL}
Inline <script> in HTML Not present — no hashes needed

If inline scripts are added in the future, you'll need to compute their SHA-256 hash and add them to script-src.


What Was NOT Changed (and Why)

Lazy-loaded routes

Routes remain eagerly loaded. Lazy loading (React.lazy()) would reduce the initial bundle but creates a critical problem for PWA offline mode: if a route's chunk hasn't been fetched before the user goes offline, that route becomes inaccessible. Since offline mode support is planned, keeping eager loading ensures the service worker precaches all route code on first visit.

Suspense / streaming / hydration

AppRoutes.tsx already wraps the router in <Suspense>. Adding streaming SSR or selective hydration would require a server-side rendering setup (the app is currently a static SPA served via Caddy), adding significant infrastructure complexity with minimal benefit for the current architecture.

CSS splitting

The CSS remains a single 905 kB file (97 kB gzipped). Carbon Design System's styles are tightly coupled — splitting them risks broken component rendering if chunks load out of order. The gzipped size (97 kB) is acceptable.


Part 1 Results

Chunk Size Gzipped
carbon 403 kB 123 kB
react-vendor 193 kB 61 kB
utils 132 kB 46 kB
router 86 kB 29 kB
index (app code) 79 kB 26 kB
aws 72 kB 24 kB
query 36 kB 11 kB
workbox 6 kB 2 kB
Total JS 1,008 kB 322 kB
index.css 905 kB 97 kB
PWA precache 1,870 kB —
Metric Before After Improvement
Total JS 5,826 kB 1,008 kB −83%
Gzipped JS 1,611 kB 321 kB −80%
Largest chunk 5,820 kB 362 kB −94%
PWA precache total 6,574 kB 1,870 kB −72%
Build time ~18s ~10s −44%
Number of JS chunks 2 8 Better cacheability

Part 1 Files Modified

File Change
vite.config.ts Added manualChunks function
Caddyfile Split cache policy: immutable for assets, no-cache for HTML
src/components/core/DistrictSelection/constants.ts Replaced import * with explicit named imports
src/components/core/DistrictSelection/DistrictItem/index.tsx Use direct component references
src/components/core/EmptySection/index.tsx Changed API from string-based to component-based icon props
src/components/core/EmptySection/index.unit.test.tsx Updated tests for new API
src/pages/RoleError/index.tsx Pass pictogram component instead of string
src/components/Form/TableResource/index.tsx Pass pictogram components instead of strings
src/context/preference/PreferenceProvider.tsx lodash → lodash/isEqual, lodash/mergeWith
src/context/auth/AuthProvider.tsx lodash → lodash/isEqual
src/hooks/useSyncPreferencesToFilters/index.tsx lodash → lodash/isEqual
src/services/types.ts Added type keyword to type-only re-exports

Part 2: Font & CSS Performance Optimization

Date: April 8, 2026 Scope: Lighthouse mobile performance (FCP, LCP, Speed Index) Impact: Reduced CSS from 884 KB → 567 KB (raw), 96.7 KB → 63.2 KB (gzip). Eliminated 82 unused @font-face declarations and ~60 unused Carbon component stylesheets. Added font-display: swap, font preloading, deferred render-blocking scripts, and fixed bfcache.


Problem

Lighthouse mobile performance scored 58 with poor Core Web Vitals:

Metric Before
FCP 7.9s
LCP 8.2s
Speed Index 7.9s

Root cause analysis identified font loading as a major contributor:

  1. 82 unused IBM Plex @font-face declarations were embedded in the CSS bundle by Carbon Design System, adding parse time and CSS size despite the app using BCSans exclusively.
  2. BCSans fonts lacked font-display: swap, causing invisible text (FOIT — Flash of Invisible Text) while fonts downloaded, directly blocking FCP and LCP on slow connections.
  3. BCSans @font-face listed .woff before .woff2, meaning browsers downloaded the larger format (~350 KB each) instead of the smaller .woff2 (~230 KB each) — a ~35% size penalty per font variant.
  4. No font preloading — the browser could only discover font URLs after parsing the entire CSS file.

Changes

1. Disabled IBM Plex font-face emission

File: src/styles/index.scss

Added Carbon's $css--font-face: false configuration before any Carbon import:

@use '@carbon/styles/scss/config' with ($css--font-face: false);

Why: @use '@carbon/react' in _overrides.scss imports the full Carbon stylesheet, which includes @carbon/styles/scss/fonts. That module emits @font-face rules for IBM Plex Mono (52 declarations) and IBM Plex Sans (30 declarations) — none of which are used since the app's type family is configured to use BCSans. Setting $css--font-face: false before any Carbon module loads prevents this emission entirely.

Result: 82 @font-face blocks removed. The --cds-code-*-font-family CSS custom properties still reference "IBM Plex Mono" as a name in the font stack, but without @font-face declarations the browser falls through to the next family (system-ui), which is the correct behavior.

2. Added font-display: swap to BCSans

File: src/styles/_fonts.scss (new)

Created a new SCSS partial with override @font-face declarations for all 4 BCSans variants that include font-display: swap.

Why: The upstream @bcgov-nr/nr-theme package defines BCSans @font-face rules without font-display, which defaults to auto (effectively block on most browsers). This tells the browser to show invisible text for up to 3 seconds while the font downloads. On mobile with slower connections (3G/4G), this directly blocks FCP and LCP.

With font-display: swap, the browser immediately renders text using a fallback system font, then swaps to BCSans once it finishes downloading. Users see content instantly instead of a blank page.

Result: Text is visible on first paint using fallback fonts; BCSans swaps in once loaded.

3. Fixed font format source order (woff2-first)

File: src/styles/_fonts.scss

The override @font-face declarations list only woff2 format, with the files served from the public/fonts/ directory.

Why: The upstream nr-theme declares src: with .woff listed before .woff2. Browsers use the first supported format in the list. Since all modern browsers support both, they download the larger .woff files. Per-variant savings:

Variant woff woff2 Saving
Regular 346 KB 226 KB 120 KB
Bold 389 KB 229 KB 160 KB
Italic 364 KB 233 KB 131 KB
BoldItalic 369 KB 238 KB 131 KB

Our override declarations appear later in the CSS, so the browser uses them instead of the upstream ones.

Result: Browsers download woff2 exclusively — up to 542 KB total savings across all 4 variants.

4. Preloaded BCSans Regular weight

Files:

<link rel="preload" as="font" type="font/woff2" href="/fonts/BCSans-Regular.woff2" crossorigin />

Why: Without preload, the browser must: download HTML → parse HTML → discover CSS link → download CSS → parse CSS → discover @font-face URL → download font. The <link rel="preload"> tells the browser to start downloading the Regular weight font immediately when parsing the HTML <head>, in parallel with the CSS download.

The Regular weight is preloaded because it's used for the majority of body text and is the only variant needed for the initial render. Bold, Italic, and BoldItalic are loaded on-demand when the CSS triggers them.

Font files are served from public/fonts/ with deterministic paths (no content hash in filename) so the preload href matches the @font-face src URL exactly. The Caddyfile was updated to cache /fonts/* aggressively (max-age=31536000, immutable) matching the existing /assets/* policy.

5. Preloaded BCSans Bold weight

File: index.html

<link rel="preload" as="font" type="font/woff2" href="/fonts/BCSans-Bold.woff2" crossorigin />

Why: Lighthouse critical path analysis showed BCSans-Bold.woff2 as the longest chain at 1,991ms — it's discovered only after HTML → CSS → parse CSS → find @font-face. The Bold weight is used for headings and the navigation header, which are above-the-fold content. Preloading starts the download in parallel with CSS, removing it from the critical chain.

6. Deferred render-blocking scripts

File: index.html

<script defer src="/data/config.js"></script>
<script defer src="/data/params.js"></script>

Why: config.js and params.js were synchronous <script> tags in <head>, blocking HTML parsing and first paint for 470ms each (Lighthouse data). These scripts only set window.config = {} (runtime environment defaults). Adding defer allows:

  • HTML parsing to continue without waiting for these scripts
  • First paint to happen before script execution
  • Execution still in document order, and before type="module" scripts (guaranteed by spec)

This is safe because the app consumes window.config via env.ts, which is imported by the module entry point — defer scripts always execute before module scripts.

7. Fixed bfcache and font caching

File: Caddyfile

Changed the non-asset cache policy from no-store to no-cache:

@notassets {
    not path /assets/*
    not path /fonts/*
}
header @notassets Cache-Control "no-cache, must-revalidate, proxy-revalidate"

Why (bfcache): Cache-Control: no-store explicitly prevents browsers from storing the page in bfcache (back/forward cache). Lighthouse flagged this as 3 failure reasons. Changing to no-cache still forces revalidation on every navigation (the page is never served stale) but allows the browser to use bfcache for instant back/forward navigations. This is a significant UX win on mobile.

Why (font caching): The previous @notassets matcher only excluded /assets/* but not /fonts/*, meaning our font files at /fonts/BCSans-*.woff2 were being served with no-store instead of the intended aggressive caching. Adding not path /fonts/* ensures fonts get the separate aggressive cache rule.

8. Selective Carbon component CSS imports

File: src/styles/_overrides.scss

Replaced the blanket @use '@carbon/react' with:

  1. Base modules: reset, layer, layout, zone
  2. Individual component imports for only the ~22 components actually used

Why: @use '@carbon/react' forwards to @carbon/styles, which imports CSS for all ~80 Carbon components. The app uses only ~22 of them. Lighthouse reported 95.5 KiB of 104.4 KiB (91%) of CSS as unused on the /search page. The selective import approach includes only:

breadcrumb, button, checkbox, combo-box, data-table (+ action/expandable/skeleton/sort), date-picker, dropdown, form, link, list-box, loading, modal, multiselect, notification, overflow-menu, pagination, popover, search, select, skeleton-styles, stack, tag, text-input, tooltip, ui-shell

Result: CSS dropped from 852 KB → 567 KB (raw), from 93.3 KB → 63.2 KB (gzip) — a 32% reduction in transfer size.

Maintenance note: When adding a new Carbon component to the app, its SCSS module must also be added to _overrides.scss. If a new Carbon component renders without its expected styles, add its corresponding @use '@carbon/styles/scss/components/<name>' line.

9. Moved images to public/img/, converted logos to SVG, compressed landing image

Files:

Why (move to public/img/): Vite hashes asset filenames in production (e.g., landing-BxfW581V.webp), which means <link rel="preload"> in index.html cannot reference them with static paths. Moving images to public/img/ gives deterministic URLs that the browser's preload scanner discovers immediately in the initial HTML — before any JavaScript executes. This directly addresses the Lighthouse "request is not discoverable in initial document" audit.

Why (SVG logos): The raster logos required multiple density variants (1x, 2x, 3x) for different device pixel ratios — 6 WebP files totaling ~70 KB. SVGs scale to any resolution with a single file (13 KB each), eliminating srcSet complexity entirely.

Why (landing image recompression): The previous WebP conversion used quality=85. Reducing to quality=50 with method=6 (best Pillow compression) saved 20%. The image is a decorative hero displayed with object-fit: cover, so the lower quality is visually acceptable.

Why (precaching): Added webp (and svg implicitly via the glob) to the injectManifest.globPatterns in vite.config.ts so the service worker precaches all images on install. This ensures instant image loading on repeat visits and offline support.

Why (/img/* caching): Unlike /assets/* (content-hashed, immutable), files in /img/ have human-readable names that may change. A 24-hour cache with must-revalidate balances performance with the ability to update images without cache-busting.

Image Before (raster) After (SVG/compressed) Savings
bc-gov-logo (all variants) 6 WebP + 2 PNG (~170 KB) 1 SVG (13 KB) 92%
bc-gov-logo-rev (all variants) 6 WebP + 2 PNG (~164 KB) 1 SVG (13 KB) 92%
landing 162 KB (WebP, q=85) 129 KB (WebP, q=50) 20%
Total ~496 KB ~155 KB 69%

10. Cypress Lighthouse now auto-selects mobile/desktop profile

File: ../cypress/cypress/support/helpers/lighthouse.helpers.ts

Implemented platform-aware Lighthouse options in Cypress so audits match the target execution mode:

  1. runReportTo() now resolves and passes full Lighthouse options (not only URL) to cy.runLighthouseAudit(...).
  2. Added resolveLighthouseFormFactor() to infer mobile or desktop from Cypress runtime context in this order:
    • lighthouseFormFactor
    • formFactor
    • platform
    • targetPlatform
    • deviceType
    • fallback: viewport width (<= 768 => mobile, else desktop)
  3. Added resolveLighthouseRunOptions() to keep formFactor and screenEmulation aligned:
    • mobile profile: formFactor: "mobile", screenEmulation.mobile: true (390x844, dpr 2)
    • desktop profile: formFactor: "desktop", screenEmulation.mobile: false (1920x1080, dpr 1)

Why: Lighthouse warns when formFactor and screenEmulation.mobile disagree (for example, desktop + mobile: true). Aligning both values removes configuration mismatch noise and ensures metrics are collected against the intended platform profile.

Result: The same test steps can run against desktop or mobile targets without code changes in step definitions. Platform is controlled by Cypress env/viewport and propagated automatically to Lighthouse.

11. Cypress Lighthouse parity fixes and config diagnostics

Files:

Added three runtime/reporting changes to reduce score drift between Chrome DevTools Lighthouse and Cypress Lighthouse runs and make differences inspectable:

  1. Option-aware Lighthouse report cache key in cypress.config.ts

    • Previously, in-memory caching used only normalized URL.
    • Now cache key is URL + stable-serialized Lighthouse options, preventing desktop/mobile profile collisions for the same route.
  2. Desktop preset loading when formFactor === "desktop" in cypress.config.ts

    • lighthouse/core/config/desktop-config.js is loaded and passed to the Lighthouse Node API for desktop runs.
    • This aligns Cypress closer to explicit desktop audits instead of relying only on flag-level formFactor and screenEmulation overrides.
  3. Effective config visibility in reports

    • lighthouseConfigSettings from lhr.configSettings is now included in report payloads/events.
    • lighthouseModule.js now prints an Effective Config line per path/profile in the markdown summary and groups by both lighthouseOptions and lighthouseConfigSettings.

Why: We observed significant score mismatch (e.g., Chrome ~91 vs Cypress ~57). Main causes were mixed profile reuse by URL-only caching and insufficient visibility into effective Lighthouse runtime settings.

Result: Desktop/mobile runs no longer overwrite each other in cache, Cypress desktop audits use the desktop config preset, and summary output now shows the concrete settings used to compute each score.


Part 2 Build Output Comparison

Metric Before After
CSS raw size 884 KB 567 KB
CSS gzipped ~96.7 KB ~63.2 KB
@font-face blocks in CSS 86 (82 IBM Plex + 4 BCSans) 8 (4 BCSans original + 4 BCSans override)
font-display: swap 0 4
Font format on first match woff (~350 KB/variant) woff2 (~230 KB/variant)
Render-blocking scripts 2 (config.js + params.js) 0 (deferred)
bfcache support Blocked (no-store) Enabled (no-cache)
Image assets total 434 KB (PNG) 155 KB (SVG + WebP)

Part 2 Files Changed

File Change
src/styles/index.scss Added @use '@carbon/styles/scss/config' with $css--font-face: false; added @use './fonts'
src/styles/_fonts.scss New — BCSans @font-face overrides with font-display: swap and woff2-only
src/styles/_overrides.scss Replaced @use '@carbon/react' with selective base + component imports
index.html Added <link rel="preload"> for fonts, SVG logos, and landing image; added defer to config/params scripts
public/fonts/ New directory — BCSans woff2 files for preload-compatible serving
public/img/ New directory — SVG logos + compressed landing.webp (moved from src/assets/img/)
src/assets/.gitkeep New — placeholder to keep the empty assets directory in git
Caddyfile Added /fonts/* and /img/* caching; excluded both from no-cache rule; changed no-store → no-cache for bfcache
vite.config.ts Added globPatterns with webp to injectManifest for precaching
src/pages/Landing/index.tsx Switched to static /img/ paths with SVG logos; removed asset imports; added height and fetchPriority="high"
src/pages/Landing/index.unit.test.tsx Updated assertions for SVG paths
cypress/cypress/support/helpers/lighthouse.helpers.ts Added automatic Lighthouse profile resolution from Cypress env/viewport and passed aligned formFactor + screenEmulation into runReportTo
cypress/cypress.config.ts Added option-aware Lighthouse cache keys, desktop preset loading, and lighthouseConfigSettings emission in report payloads
cypress/scripts/modules/lighthouseModule.js Added profile/config reporting (Effective Config) and grouping/dedupe by both Lighthouse options and config settings

Notes

  • The upstream @bcgov-nr/nr-theme package should ideally be updated to include font-display: swap and woff2-first ordering. Once that happens, the _fonts.scss override file can be removed.
  • @carbon/charts and @carbon/charts-react are listed as dependencies in package.json but are not imported anywhere in the source code. Removing them would reduce node_modules size and eliminate potential accidental future imports.
  • When adding new Carbon components, remember to add their SCSS module to _overrides.scss (see maintenance note in Part 2, section 8).
  • Further JS reduction is possible by lazy-loading route-level chunks (e.g., splitting the /search page and landing page into separate dynamic imports via React.lazy). The Lighthouse report flagged 473 KiB of unused JS, primarily from carbon.js (77 KiB savings) and utils.js (28 KiB savings) — these are components and utilities loaded eagerly but not used on the initial page.
  • The Lighthouse "Modern HTTP" suggestion (HTTP/2 or HTTP/3) is an infrastructure concern — Caddy supports HTTP/2 natively over TLS, but auto_https is disabled in favor of OpenShift route-level TLS termination. Enabling HTTP/2 at the route level would allow multiplexing all the JS/CSS/font requests over a single connection.

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally