WWW CSS Performance Plan (Top 25 Pages)
Created: March 2026 Scope: WWW bundles only (not CRM) Related docs: Performance CSS Bundles | Dead CSS Removal Workflow | FA7 Optimization Report | Asset Build Workflow
Purpose
Section titled “Purpose”Reduce CSS weight and improve load performance on the pages that matter most: the top 25 most-visited WWW pages. Success is measured by gains on representative URLs from that list, not sitewide averages.
All WWW pages share a single global pipeline:
www.bundler.js └─ www.index.scss ├─ bs.scss → full Bootstrap 5.3 (customised variables) ├─ FA Pro CSS → 5 font families (solid, regular, sharp-solid, sharp-regular, brands) ├─ 00-variables/ → project tokens ├─ 01-base/ → resets + typography ├─ 02-objects/ → layout objects (navbar, footer, etc.) ├─ 03-components/ → large barrel (_components.scss — ~40 partials) ├─ 04-utilities/ → custom utility extensions ├─ Uppy CSS → file upload styles └─ Fancyapps CSS → carousel + lightbox stylesThe global www CSS bundle is approximately 628 KB (production, parsed). Every page on the site pays for the full weight whether it uses the imported modules or not.
How we define “top 25”
Section titled “How we define “top 25””Use one of these sources (GA4 recommended for broadest traffic view) and refresh quarterly.
| Source | API / query | Best for |
|---|---|---|
| GA4 | Seo::Ga4ApiClient#top_pages (ordered by screenPageViews) | All traffic including paid, direct, email |
| First-party visits | SiteMap ordered by visit_count_30d (fed by Seo::VisitsSyncService) | Server-verified visits aligned to internal paths |
| Organic only | Ahrefs top pages (Seo::AhrefsApiClient#top_pages) | SEO-focused prioritization |
Export exact paths (including locale prefix like /en-US/... if treating locales separately). Deduplicate paths that resolve to the same controller/template.
Workflow
Section titled “Workflow”flowchart LR exportList["Export top 25 paths"] groupTemplates["Group by template"] pickReps["Pick representative URLs"] baseline["PSI + Coverage baseline"] globalTier["Optimize shared WWW CSS"] splitTier["Optional: split barrels"] exportList --> groupTemplates --> pickReps --> baseline --> globalTier --> splitTier- Export the top 25 page paths from GA4 (last 28-90 days).
- Group by controller/template — many paths share the same layout (e.g. all blog posts use one template).
- Pick representatives — one URL per template group to keep the test matrix small (aim for 10-15).
- Baseline — run PSI and Chrome Coverage on each representative (see Measurement below).
- Optimize — work through the tiers below, re-measuring after each meaningful change.
Measurement
Section titled “Measurement”Run all measurements against the same representative URLs with a consistent device profile.
| Tool | What it tells you | How to run |
|---|---|---|
| Webpack treemap | Module-level breakdown of JS and CSS bundles | mise exec -- yarn analyze:webpack then open tmp/webpack-bundle-report.html |
| PSI / Lighthouse | LCP, CLS, unused-CSS opportunity | PageSpeed Insights (mobile, same throttling each time) |
| Chrome Coverage | Session-level used vs unused bytes per stylesheet | DevTools → Coverage panel; include user interactions on tool pages |
Record each baseline in the table below (or a linked spreadsheet). Re-run after meaningful CSS changes and compare same URL, same profile.
Interpreting “unused CSS”: Lighthouse will flag Bootstrap utilities and global component styles that are used on other routes. Treat the number as a trend indicator, not a delete list. See Dead CSS Removal Workflow for safe procedures.
Phased work
Section titled “Phased work”Tier A — Global CSS (highest leverage)
Section titled “Tier A — Global CSS (highest leverage)”Every top-25 page pays for these. Reducing them yields the broadest impact.
| Area | Current state | Action |
|---|---|---|
| Bootstrap | Full @forward "bootstrap/scss/bootstrap" via bs.scss | Audit which Bootstrap modules are actually used across templates. Moving to a curated partial import is a large change — requires class-level grep of app/views/, components, and Stimulus DOM. Defer until Coverage data identifies the heaviest unused modules. |
_components.scss barrel | ~40 partials imported globally | Identify partials that only serve one or two templates (e.g. page-checkout, page-smartplan). Candidates for extraction into route-scoped CSS entries or lazy-loaded chunks. |
| Font Awesome | 5 CSS families imported in www.index.scss | Inventory which FA families appear in ERB/components across the top-25 templates. Drop any family with zero references. See FA7 Optimization Report for prior work. |
| Fancyapps CSS | Lazy-loaded | fancy_carousel_controller.js already imports @fancyapps/ui/dist/carousel/*.css on demand alongside each plugin’s JS, so only carousel-bearing pages pay for it. Keep this pattern when adding new plugins (Lazyload, Toolbar, etc.). |
| Uppy CSS | Global import | Only a handful of routes use file uploads — candidate for lazy-loading alongside the Uppy JS in the same way Fancyapps now does. |
| Orphan SCSS | Ongoing | Continue removing unreferenced partials (rg for basename + @use/@import paths; delete only when confirmed unused). |
Tier B — Template-driven (after bucketing the top 25)
Section titled “Tier B — Template-driven (after bucketing the top 25)”Once the 25 pages are grouped by template, optimizations can target the heaviest template buckets:
- Blog / content — Usually typography + cards + nav. Coverage may show that many
_components.scsspartials are cold on these routes. - Homepage — Already has a separate
wwwHomeStylesentry; check for overlap with the global bundle. - Quote builder / tools — Heavier on JS; CSS weight is secondary to interaction performance (INP). Ensure lazy-loaded chunks cover tool-specific styles.
Tier C — Strategic (separate initiative)
Section titled “Tier C — Strategic (separate initiative)”- PurgeCSS — Only with comprehensive safelists (Bootstrap utilities, dynamic classes, CMS content, Stimulus-added classes). Treat as its own project with full QA and rollback. See Dead CSS Removal Workflow.
- Critical CSS — Only if LCP on key pages is dominated by render-blocking CSS after Tier A/B work. Measure first.
Top 25 pages (en-US)
Section titled “Top 25 pages (en-US)”Source: GA4 property 252042830 (screenPageViews, 90-day window ending March 23 2026). Locale duplicates (en-CA) excluded; paths stripped to route-level for controller mapping. Refresh quarterly.
| Rank | Page path | Views (90d) | Controller | Bucket | Rep |
|---|---|---|---|---|---|
| 1 | / | 16,877 | pages#show (home) | global-heavy | Y |
| 2 | /floor-heating | 15,611 | pages#show | global-heavy | Y |
| 3 | /quote-builder | 9,843 | www/quote_builder#show | tool | Y |
| 4 | /snow-melting/heated-driveway | 9,306 | pages#show | global-heavy | |
| 5 | /towel-warmer | 8,526 | www/towel_warmers#index | global-heavy | Y |
| 6 | /floor-plans/bathroom | 8,181 | www/floor_plan_displays#by_room_type | tool | Y |
| 7 | /snow-melting | 7,822 | pages#show | global-heavy | |
| 8 | /snow-melting/quote-builder | 7,060 | www/quote_builder#show | tool | |
| 9 | /posts/How-to-Calculate-the-Cost-of-a-Heated-Driveway-1181 | 6,951 | posts#show | content | Y |
| 10 | /products/code/WHMA-120-0305 | 6,000 | www/products#code | global-heavy | Y |
| 11 | /posts/9-pros-and-cons-of-heated-floors | 5,714 | posts#show | content | |
| 12 | /floor-heating/quote-builder | 5,429 | www/quote_builder#show | tool | |
| 13 | /my_cart | 4,297 | my_carts#show | tool | Y |
| 14 | /products/code/WHCA-120-0043 | 4,171 | www/products#code | global-heavy | |
| 15 | /products/code/WHMA-120-0205 | 3,205 | www/products#code | global-heavy | |
| 16 | /posts/How-Much-Does-Floor-Heating-Cost-2574 | 3,107 | posts#show | content | |
| 17 | /radiant-heat-panels | 2,979 | www/radiant_panels#index | global-heavy | Y |
| 18 | /quote | 2,871 | www/leads#quote | global-heavy | Y |
| 19 | /posts/remove-driveway-ice-without-salt | 2,816 | posts#show | content | |
| 20 | /tools/online-design-tool | 2,643 | pages#show | global-heavy | Y |
| 21 | /posts | 2,633 | posts#index | content | Y |
| 22 | /posts/bathroom-floor-heating-... | 2,633 | posts#show | content | |
| 23 | /floor-heating/thermostats | 2,551 | pages#show | global-heavy | |
| 24 | /floor-heating/heated-floor-mat | 2,511 | pages#show | global-heavy | |
| 25 | /product-reviews | 2,481 | www/reviews#index (redirect) | global-heavy | Y |
Template group summary
Section titled “Template group summary”| Template | Controller | Count | Combined views | Rep URL for testing |
|---|---|---|---|---|
| CMS static page | pages#show | 9 | 67,189 | /floor-heating |
| Blog post | posts#show | 6 | 21,221 | /posts/How-to-Calculate-the-Cost-of-a-Heated-Driveway-1181 |
| Quote builder | www/quote_builder#show | 3 | 22,332 | /quote-builder |
| Product page | www/products#code | 3 | 13,376 | /products/code/WHMA-120-0305 |
| Towel warmer PLP | www/towel_warmers#index | 1 | 8,526 | /towel-warmer |
| Floor plans | www/floor_plan_displays#by_room_type | 1 | 8,181 | /floor-plans/bathroom |
| Cart | my_carts#show | 1 | 4,297 | /my_cart |
| Blog index | posts#index | 1 | 2,633 | /posts |
13 representative URLs cover all template groups for PSI/Coverage testing.
Bucket values: global-heavy (layout + nav + footer dominant), content (blog, CMS), tool (quote builder, calculators, room planner).
Rep: Y marks the URL that represents its template group in PSI/Coverage testing.
Out of scope
Section titled “Out of scope”- CRM bundles — Separate entry point, separate audience. Duplicate Bootstrap across WWW and CRM is expected and intentional.
- Third-party script weight (GTM, analytics pixels) — Not controlled by CSS pipeline.
- Server-side rendering or caching — Orthogonal to stylesheet optimization.