perf: speed up mobile LCP on trading-view token & pair pages - #1288
Merged
Conversation
Reduce mobile Largest Contentful Paint on the trading-view token and trading-pair detail pages (top offenders in Search Console Core Web Vitals) by cutting server response time, improving edge cache reuse, trimming the critical path, and unblocking font loading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
miohtama
added a commit
that referenced
this pull request
Jun 25, 2026
## Why PR #1288's CI repeatedly failed on 3 tests in `tests/integration/trading-view/vaults/datasets.test.ts` (the vault-datasets API-key flow) — a different overlapping subset each run — while they pass reliably locally. This is long-standing: the CI workflow itself notes (since 2023) that the integration suite is *"too flaky and do not pass on CI at all"*, and `build`'s `needs: test` is commented out because of it. **Root cause (reproduced, not guessed):** the integration suite drives the whole app against a single-process `vite preview` server whose APIs are served by an in-process mock middleware (`vite-plugin-mock-dev-server`). Under the parallel-worker load CI applies, that shared server intermittently mishandles a request. The captured failure snapshot shows the form displaying **"The API key is not valid"** after submitting the *valid* key — i.e. the mock returned 401 for a correct key under contention. It's environmental, not an app bug. Reproduced locally by running the file under `--workers=4 --repeat-each=8`: **7/176 runs failed** with the exact CI symptom. Re-running the identical stress with `--retries=2`: **0 hard failures** (all flakes self-recovered). The integration/e2e Playwright configs had **no `retries` set**, so CI ran with 0 retries — any single transient blip failed the whole job. ## Lessons learnt - **The integration harness shares one preview+mock process across all Playwright workers.** Parallel load causes occasional request mishandling (here: a valid API key mock-rejected with 401). Treat integration/e2e flakiness on CI as environmental contention first; reproduce with `--workers=N --repeat-each=M` before suspecting app code. - **Retries are the standard mitigation for inherently flaky server-backed e2e/integration suites** — but only on CI. Keep retries at 0 locally so genuine regressions surface immediately instead of being masked. - The failure surfaces as either a 5s assertion timeout *or* a spurious "API key not valid" error — both trace back to the same shared-server contention, so a deterministic `waitForResponse` in the test would not have fixed the 401 case; retrying does. - If the flakiness needs eliminating at the source later, the lever is reducing contention (e.g. fewer integration workers) or a more concurrency-robust mock, not the app. ## Summary - Added a shared `ciRetries` constant in `tests/helpers.ts` (`process.env.CI ? 2 : 0`) with a comment documenting the root cause. - Wired `retries: ciRetries` into all three Playwright configs: `tests/integration/playwright.config.ts`, `tests/integration/private.playwright.config.ts`, and `tests/e2e/playwright.config.ts`. - No app/source changes — test-infrastructure only. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Google Search Console flagged a mobile Core Web Vitals issue — LCP longer than 2.5s — affecting 411 URLs, with the worst offenders being trading-view token detail pages (
/trading-view/[chain]/tokens/[address]) and trading-pair pages (/trading-view/[chain]/[exchange]/[pair]), measured at 2.6–3.4s.Investigation showed the LCP element on these pages is the
<h1>page heading (text), not the chart. A third affected URL group is a static/docs/page with no SvelteKit and no chart, yet equally slow — which points the diagnosis at factors shared across all pages: server response time (TTFB), edge-cache reuse, and render-blocking web fonts, rather than the chart/JS bundle.Lessons learnt
font-display: swapmeans font preloading does not move the LCP timestamp. With swap, the heading paints immediately in a fallback font; the web-font swap is a later repaint. An earlier attempt to preload specific weights was dropped — it added fragile, design-token-coupled file references and per-page "preloaded but not used" warnings for zero LCP gain. The real font lever is that the font stylesheet was render-blocking; making it non-blocking is what helps.fonts5.cssis purely@font-face, so it can safely load non-render-blocking (media="print"→onload) without affecting element geometry.token/detailsand the Typesensereservessearch were awaited sequentially, but the reserve search keys off the URL address, not the token response — they are independent and can run in parallel.max-agemostly misses the cache → cold SSR per visit.stale-while-revalidateserves a cached copy instantly and refreshes in the background.frame-ancestors: self(noscript-src), so the inlineonloadnon-blocking-CSS pattern is permitted; a<noscript>fallback covers JS-off.Summary
tokens/[token]/+page.ts— fetchtokenandreservesin parallel viaPromise.allinstead of a sequential waterfall, so SSR (and the LCP heading) is gated by the slower call, not the sum.+page.tsloaders (token + pair) — addstale-while-revalidate=86400to the cache header so the Cloudflare edge serves the long tail of URLs instantly while refreshing in the background.[exchange]/[pair]/+page.svelte— lazy-load the below-the-foldPairCandleChartvia{#await import(...)}, movinglightweight-chartsoff the page's static critical path (verified split into a dynamically-imported client chunk).app.html— loadfonts5.cssnon-render-blocking (media="print"→onload="this.media='all'") with a<noscript>fallback, removing a render-blocking resource from first paint.PageHeader.svelte— consolidate the heading to a single weight: dropped the.multilinefont-weight: 700override so all page headers use their--f-h1-medium(600) design token, instead of mixing 600 and 700.🤖 Generated with Claude Code