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.ymland 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.5son representative public campaign pages - INP under
200msfor 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:
_layouts/default.html: shared public layout_layouts/campaign.html: campaign detail layout_includes/cart-runtime-foot.html: lightweight cart loader include_includes/page-prefetch.html: public document prefetch includeassets/js/cart-runtime-loader.js: lazy cart runtime bootstrapassets/js/page-prefetch.js: intent-based document prefetch runtimescripts/minify-site-assets.mjs: generated CSS/JS minificationscripts/sync-worker-config.rb: site-to-Worker config mirroring
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
deferor lazy dynamic loading instead of parser-blocking script tags - private/admin surfaces stay
noindexand 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:
assets/js/cart-provider.jsassets/js/cart.jsassets/js/buy-buttons.js- checkout sidecars and shared add-on/shipping helpers
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:
pointeroverafterperformance.intent_prefetch_delay_msfocusinafterperformance.intent_prefetch_delay_mstouchstartimmediately
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
targetother 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, orsession - 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.visibilityStateis notvisiblenavigator.connection.saveDatais truenavigator.connection.effectiveTypeisslow-2gor2g- 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:
- Enable
performance.intent_prefetch_enabled: truein a staging config. - Confirm public campaign-card links create
link[rel="prefetch"][as="document"]after hover or focus. - Confirm admin, manage, checkout, pledge-result, community, tokenized, external, and
target="_blank"links do not prefetch. - Check DevTools Network with throttling and save-data style conditions.
- 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/performanceto 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:v1after the first sold-count projection bootstrap - launch reminder dispatch uses
launch-reminder-dispatch-queue:v1so idle scheduled ticks do not listlaunch-reminder-dispatch:* - supporter confirmation email retry uses
supporter-email-retry-queue:v1so retry polling skipssupporter-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, and1600wfor public image templates when the source image is larger than that variant - skips
cwebpre-optimization for animated WebP derivatives becausecwebpcannot decode animated WebP files - generates WebM derivatives for uploaded videos
- rewrites literal
_campaigns/_config.ymlreferences 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_enabledperformance.intent_prefetch_delay_msperformance.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
_siteassets passnpm 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:checkwhen uploaded or manually added media changed - relevant unit and browser tests pass against built assets