Performance

The Pool is a static-first crowdfunding platform with a Cloudflare Worker for mutations, live reads, and admin operations. Performance work should preserve that shape: public pages should be fast from static HTML, heavy application code should load only when a user needs it, and speculative work should stay conservative enough that it never makes checkout, admin, or supporter flows less reliable.

This guide covers the current platform performance model, the knobs forks can tune, and the validation expected before shipping performance changes.

Principles

  • keep first paint and the primary campaign action usable before optional runtime code finishes
  • prefer static Jekyll output, Cloudflare edge caching, and browser caching before adding client complexity
  • avoid loading checkout, cart, admin, or management code until the user expresses intent
  • keep public pages crawlable and functional without relying on JavaScript for core content
  • never speculate on private, tokenized, checkout, admin, or supporter routes
  • make performance features configurable from _config.yml and the admin dashboard where forks may need different traffic tradeoffs
  • measure changes against real built assets, not only source files

Targets

Use these as practical targets rather than claims that every local test run will hit them:

  • LCP under 2.5s on representative public campaign pages
  • INP under 200ms for campaign, cart, checkout, manage, and admin interactions
  • CLS under 0.1, with progress bars, hero media, tier cards, and live stats reserving stable space
  • no eager full cart stack on an anonymous public first load
  • no public document prefetches on private, tokenized, checkout, admin, manage, or supporter-community routes
  • generated CSS/JS assets pass npm run assets:minify:check
  • Cloudflare serves text assets with transfer compression and without Auto Minify

Platform Model

The public site is generated by Jekyll and deployed as GitHub Pages output. The Worker handles dynamic concerns such as checkout intent, live stats, inventory, pledge management, admin publishing, reports, and observability.

Important repo surfaces:

Critical Rendering

Public campaign pages should avoid layout shifts and late-discovered critical resources.

Current guardrails:

  • progress bars and marker positions render static width/left utility classes in Jekyll output so they do not start collapsed while JavaScript loads
  • campaign hero images are emitted with preload and high fetch priority where the layout knows the likely LCP asset
  • YouTube campaign hero videos render a local poster/play facade first and load the YouTube iframe only after play intent
  • common scripts use defer or lazy dynamic loading instead of parser-blocking script tags
  • private/admin surfaces stay noindex and should not inherit public prefetch behavior

When changing campaign chrome, verify:

  • the progress bar does not flash with all ticks and labels at the left edge
  • the LCP image is discoverable early in the document
  • text and controls do not shift after live stats or inventory hydrate
  • JavaScript failure still leaves campaign content, tier copy, and primary links readable

Runtime Loading

The cart runtime is intentionally split. Public pages load a small loader first, then fetch the heavier cart stack only when needed.

The loader should trigger on:

  • add-to-cart button interaction
  • persisted cart state that needs restoration
  • checkout recovery state
  • cart UI intent, such as opening the cart

The heavy cart files should not be part of an ordinary public first load unless one of those states is present:

When changing cart or checkout loading, verify with browser network tools that an anonymous campaign page view does not eagerly download the full cart stack.

Generated Asset Minification

Repository source files stay readable. Production deploys minify generated output after Jekyll writes _site.

The deploy workflow runs:

npm run assets:minify

That command rewrites smaller _site/assets/**/*.css and _site/assets/**/*.js files before the GitHub Pages artifact is uploaded. JavaScript minification is conservative: it removes whitespace and simplifies syntax, but does not mangle properties or rewrite identifiers. CSS is minified after Sass has already emitted compressed output.

Use this check after a local build when changing generated asset handling:

npm run assets:minify:check

The pre-merge build-artifact check also minifies _site and fails if generated CSS/JS still has minification savings.

Cloudflare Compression

Cloudflare should handle transfer compression at the edge. The live deployment has been verified serving compressed text assets with gzip, Brotli, and Zstandard depending on the request Accept-Encoding and edge behavior.

Keep these responsibilities separate:

  • repo build: minify generated CSS/JS
  • Cloudflare edge: gzip/Brotli/Zstandard transfer compression
  • source control: readable source files, not committed generated minified copies

Cloudflare Auto Minify should stay disabled. It rewrites responses at the edge, making production behavior harder to reproduce locally and harder to test in CI. Prefer the repo-controlled generated asset step instead.

Keep Rocket Loader and Email Address Obfuscation disabled for this site. Rocket Loader rewrites script tags at the edge, while Email Address Obfuscation injects /cdn-cgi/scripts/*/cloudflare-static/email-decode.min.js; both make strict-CSP pages harder to reproduce locally and can show up as render-blocking or console-noise diagnostics in PageSpeed Insights.

If Cloudflare Web Analytics is enabled, campaign pages must allow Cloudflare’s analytics script and beacon endpoint in the campaign CSP. Private/admin surfaces should stay stricter unless there is an explicit analytics/privacy decision to include them.

Font stylesheets are linked from the document head instead of imported from assets/main.css. This lets the browser discover font CSS and font connections without waiting on the main stylesheet while preserving the intentional font-loading behavior.

The generated design-token CSS variables are included in assets/main.css; assets/theme-vars.css remains available as a compatibility artifact, but public layouts should not request it as a separate render-blocking stylesheet.

Intent-Based Prefetching

The Pool includes an optional same-origin document prefetch runtime for public navigation links. It is inspired by instant.page’s hover/touch intent model, but the implementation is local, small, and deliberately conservative.

The runtime lives at assets/js/page-prefetch.js. It is loaded on public page surfaces by default and stays out of private app layouts.

Configuration

The shared include is _includes/page-prefetch.html. It emits the runtime only when this config is enabled:

performance:
  intent_prefetch_enabled: true
  intent_prefetch_delay_ms: 90
  intent_prefetch_limit: 3

The include is wired into public page surfaces:

Private app layouts do not load the prefetch runtime.

These fields are also exposed in the private admin dashboard under Settings -> Advanced performance. Changing them publishes _config.yml and requires the normal site rebuild before static pages reflect the new values.

Behavior

When enabled, the runtime listens for:

  • pointerover after performance.intent_prefetch_delay_ms
  • focusin after performance.intent_prefetch_delay_ms
  • touchstart immediately

It appends one low-priority hint per eligible URL:

<link rel="prefetch" as="document" href="/campaigns/example/">

The runtime deduplicates normalized URLs, strips fragments, and stops after performance.intent_prefetch_limit successful prefetches per page view.

Eligible Routes

The allowlist is intentionally narrow. Current eligible paths are:

  • /
  • localized home routes such as /es/
  • /about/
  • /terms/
  • /creator-campaign-checklist/
  • public campaign detail pages such as /campaigns/hand-relations/
  • localized public campaign routes when generated with the same path model

The runtime rejects any link that is not a same-origin http: or https: document navigation.

Exclusions

The runtime rejects links when any of these are true:

  • the link is cross-origin
  • the link uses a non-HTTP protocol
  • the link has download
  • the link has rel="nofollow"
  • the link has a target other than _self
  • the link has data-no-prefetch
  • the navigation points at the current document, including fragment-only links
  • the URL contains sensitive query params such as token, publicToken, adminToken, orderId, email, or session
  • the path is under /admin, /manage, /community, /cart, /checkout, /checkout-intent, /pledge-success, /pledge-cancelled, /api, or /worker
  • the path is not on the public allowlist

Use data-no-prefetch for one-off exclusions on otherwise eligible public links.

Network Guards

Prefetching is skipped when:

  • the browser does not report support for rel=prefetch
  • document.visibilityState is not visible
  • navigator.connection.saveData is true
  • navigator.connection.effectiveType is slow-2g or 2g
  • the configured per-page prefetch limit is already reached

Safe Enablement

The default is enabled because the runtime only speculates on public same-origin document links after explicit user intent. Disable it with performance.intent_prefetch_enabled: false if a fork has unusual navigation rules or wants to run without speculative document requests.

Recommended validation after changing the settings:

  1. Enable performance.intent_prefetch_enabled: true in a staging config.
  2. Confirm public campaign-card links create link[rel="prefetch"][as="document"] after hover or focus.
  3. Confirm admin, manage, checkout, pledge-result, community, tokenized, external, and target="_blank" links do not prefetch.
  4. Check DevTools Network with throttling and save-data style conditions.
  5. Keep the per-page limit low. The default is 3.

Caching And Worker Reads

The platform tries to keep public read traffic cheap and responsive.

Current cache-related knobs:

cache:
  live_stats_ttl_seconds: 300
  live_inventory_ttl_seconds: 300

Campaign pages cache live stats and inventory in browser storage for those TTLs. The Worker also exposes combined live reads so public pages can hydrate campaign stats and inventory without splitting into more requests than necessary.

When changing live reads:

  • prefer one combined public read over multiple independent reads
  • invalidate browser caches after successful pledge persistence
  • keep stale recovery behavior private to the browser and avoid long-lived sensitive storage
  • use GET /admin/observability/performance to inspect sampled Worker timings on deployed or local environments

KV List Budget

Workers KV list requests are a separate free-tier budget from reads and writes. Normal public and dashboard paths should avoid namespace scans and prefer projections, indexes, or explicit queue-state markers.

Current guardrails:

  • campaign reports, supporter browsing, settlement, and repair paths prefer campaign-pledges:{slug} indexes over pledge namespace scans
  • platform add-on inventory reads use add-on-inventory-sold:v1 after the first sold-count projection bootstrap
  • launch reminder dispatch uses launch-reminder-dispatch-queue:v1 so idle scheduled ticks do not list launch-reminder-dispatch:*
  • supporter confirmation email retry uses supporter-email-retry-queue:v1 so retry polling skips supporter-email-retry:* scans while idle or before the next attempt is due
  • idle queue-state markers expire hourly, which keeps compatibility with manually inserted jobs without returning to minute-level namespace polling

Under normal no-queue traffic, expect roughly 48-75 KV list requests over 24 hours. Active launch reminder batches and due supporter email retries still list their bounded queue prefixes when actual work is pending.

Media Optimization

Dashboard uploads are source-preserving. The Worker validates uploads and commits them, then requests the Optimize dashboard media workflow for image/video uploads. It still does not run native image optimizers or FFmpeg itself.

Use the repository media pipeline for source media:

npm run media:optimize
npm run media:optimize:check

If the host machine does not have the native optimizers installed, use the Podman-backed wrappers instead:

npm run media:optimize:podman
npm run media:optimize:check:podman

The Podman site image includes ffmpeg, optipng, libjpeg-turbo-progs, gifsicle, and webp so local image compression and responsive derivative generation use the same native toolchain as the GitHub media workflow. Rebuild the image with PODMAN_REBUILD=1 after changing container package requirements.

For deployed media-heavy regressions, manually run the Optimize dashboard media GitHub Actions workflow with scope=all so existing campaign assets are optimized by the same pipeline rather than edited one-off.

If PageSpeed flags oversized campaign images that already flow through responsive-image.html, first confirm whether the corresponding -320.webp, -480.webp, -640.webp, -960.webp, and -1600.webp derivatives exist. Missing derivatives should be produced by npm run media:optimize locally or by the workflow with scope=all, not by one-off manual image edits.

The media pipeline:

  • compresses images when the optimized result is smaller
  • generates responsive WebP variants at 320w, 480w, 640w, 960w, and 1600w for public image templates when the source image is larger than that variant
  • skips cwebp re-optimization for animated WebP derivatives because cwebp cannot decode animated WebP files
  • generates WebM derivatives for uploaded videos
  • rewrites literal _campaigns / _config.yml references from uploaded source videos to generated WebM derivatives
  • keeps original source videos available for rollback or future re-encoding

For campaign pages, prefer:

  • explicit image dimensions or stable CSS aspect ratios
  • optimized hero images that match the rendered crop
  • source images near the documented target dimensions; responsive variants reduce transfer size but are not a substitute for choosing the right crop
  • WebM for hero/background video where practical
  • lazy loading for below-fold media
  • meaningful alt text for informative images

Admin And Private Surfaces

Admin, manage, checkout, pledge-result, community, and tokenized routes should optimize for correctness and privacy before speculative speed.

Rules for private surfaces:

  • do not load public document prefetching
  • do not prefetch token-bearing or order-bearing links
  • keep auth, checkout, and recovery responses private and uncached
  • keep status and error messages visible without requiring a full reload
  • avoid sending secrets, admin-only data, or supporter tokens into static generated pages

Admin performance settings currently live under Settings -> Advanced performance:

  • performance.intent_prefetch_enabled
  • performance.intent_prefetch_delay_ms
  • performance.intent_prefetch_limit

Measuring Changes

Use local checks for regressions and production-like checks for final confidence.

Local validation:

bundle exec jekyll build --config _config.yml,_config.local.yml --quiet
npm run assets:minify
npm run assets:minify:check
npm run test:unit

Focused browser validation for public UI changes:

python3 -m http.server 4100 --bind 127.0.0.1 --directory _site

Then, in another shell:

PLAYWRIGHT_EXTERNAL_SERVER=1 PLAYWRIGHT_BASE_URL=http://127.0.0.1:4100 \
  npx playwright test tests/e2e/public-page-controls.spec.ts --project=chromium

Full merge validation:

npm run test:premerge

Production or staging validation should compare:

  • LCP, INP, CLS, FCP, and TTFB
  • total request count and transferred bytes on first load
  • whether heavy cart/admin/manage scripts load only on intended routes or intent
  • Cloudflare cache status and content encoding for HTML, CSS, and JS
  • Worker performance observations for checkout, admin publish, reports, and live reads
  • request volume changes after enabling prefetching

Change Checklist

Use this checklist before merging performance changes:

  • source pages still render meaningful content before JavaScript finishes
  • no private, tokenized, checkout, or admin route is prefetched
  • generated _site assets pass npm run assets:minify:check
  • Cloudflare compression remains enabled and Auto Minify remains disabled
  • public first load avoids heavy cart/runtime files unless cart state or user intent requires them
  • progress bars, hero media, and campaign controls do not shift after hydration
  • media changes pass npm run media:optimize:check when uploaded or manually added media changed
  • relevant unit and browser tests pass against built assets