perf: Core Web Vitals — WebP backgrounds, deferred topology, lazy video embeds - #5216
Open
elevatebart wants to merge 5 commits into
Open
perf: Core Web Vitals — WebP backgrounds, deferred topology, lazy video embeds#5216elevatebart wants to merge 5 commits into
elevatebart wants to merge 5 commits into
Conversation
Astro's image optimizer (<Image>/<Picture>/getImage()) only touches images that pass through those APIs. Images referenced from a CSS `background: url(...)` — including <style> blocks in .astro and .vue files — are a documented blind spot (withastro/roadmap#1333): Vite fingerprints and copies them byte-for-byte, so a source PNG ships at full size. On the /blueprints and /plugins landing pages this shipped `bar-bg.png` as a 1,127 KiB asset (the LCP-scale background flagged by the mobile PageSpeed report). Add an `astro:build:done` integration that lets authors keep the PNG/JPEG as the source of truth in src/ (edit it freely, no optimization knowledge required) while the build serves a WebP. It re-encodes every raster referenced from a CSS url() to WebP with sharp (bar-bg.png: 1,127 -> ~40 KiB), writes it next to each copy across the assembled output, rewrites the reference, and deletes the original. Because emitted asset names are content-hashed, the rename is exact. Scoping and safety: - url()-gated: only rasters referenced from a CSS url() are converted. OG/social images live in <meta ... content="..."> (never url()), so they stay PNG/JPEG — Slack/LinkedIn/X don't reliably render WebP. An `exclude` pattern (favicon/apple-touch/og/social) is a second guard. - Leaves astro:assets-optimized images and SVGs untouched. - Runs only on `astro build`; dev is unaffected. Verified by invoking the hook on real build output: bar-bg.D4ntRXAl.png -> .webp across client + server, references rewritten, no dangling refs, OG images untouched. (The full build can't complete in this sandbox because prerendering calls api.kestra.io, which the network policy blocks; CI reaches it and runs the hook end to end.) Follow-up: the per-category blueprint banners in utils/blueprints/banners.ts use bare `.src` in runtime inline styles (not a static url()), so they're out of scope here; converting them cleanly needs getImage() at the source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wPnocqds1yTbJ5QrY49GV
Contributor
☁️ Cloudflare Worker Preview Deployed!🔗 https://ks-claude-optimize-css-background-docs.kestra-io.workers.dev 🔦 Lighthouse Benchmark
Scores (0–100, higher is better)
Core Web Vitals (lower is better)
Legend🟢 improved · 🔻 regressed · (blank) no significant change View full Lighthouse HTML report for a pageFull per-page Lighthouse Results (LHR) are attached as the |
The per-category blueprint banners (bar-bg-*.png, ~1 MB each) are consumed via a
`background-image: url(...)` inline style in BlueprintHero, so a bare `.src`
import never passes through Astro's image optimizer — it shipped the full-size
source PNG. The CSS-url() build plugin can't reach these (they're runtime inline
styles, not static url()).
Route them through getImage({ format: "webp" }) so the PNG stays the source of
truth while the browser gets a WebP. `bannerForCategory` is now async and is
awaited in each page's frontmatter (a render context) rather than at module
scope, keeping it safe under SSR (both consumers are prerender=false).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wPnocqds1yTbJ5QrY49GV
`Hero.astro` set its right-column background from a bare `.src` import of a JPEG
(`style={`background-image: url(${heroBg.src})`}`). A bare import is only
fingerprinted and copied, never optimized — so the full-size JPEG shipped.
Route it through getImage({ format: "webp" }) in the frontmatter so the JPEG
stays the source of truth while the browser gets a WebP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wPnocqds1yTbJ5QrY49GV
OrchPattern rendered the topology with `<Topology client:only="vue">`, which loaded @kestra-io/topology + @vue-flow/core (a ~1.1 MB gzipped / 5.6 MB raw chunk) on page load — even though the topology panels sit below the fold and many mobile visitors never scroll to them. On /orchestration/* this was the single biggest main-thread/JS cost (perf 48, TBT ~2.1 s in the mobile report). Add LazyTopology.vue: a ~4 KiB client:only shell that dynamically imports the heavy Topology.client.vue only once an IntersectionObserver (rootMargin 200px) reports the panel near the viewport. Verified in the build that the wrapper chunk is 4 KiB and pulls Topology.client via `import(...)`, so the 5.6 MB chunk is no longer in the initial page load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wPnocqds1yTbJ5QrY49GV
The quickstart embedded the YouTube player via a raw <iframe>, so ~460 KiB of player JS loaded on every visit (a top-of-funnel, high-traffic docs page; perf 44, TBT ~2.1 s in the mobile report). Replace the iframe with a lightweight click-to-play facade: a poster button in the markdown (no iframe in the initial HTML), a small CSP-safe script in the docs layout that injects the real iframe with autoplay on click, and a <noscript> iframe fallback so no-JS visitors still get the video. Nothing from youtube.com loads until the visitor asks for the video. The facade is generic (`.yt-facade[data-yt-id]`), so other docs videos can adopt it too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wPnocqds1yTbJ5QrY49GV
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.
Batch of Core Web Vitals fixes from the mobile PageSpeed sweep. Each optimization keeps the source asset / authoring format unchanged — the build or runtime does the optimizing, so no per-asset knowledge is needed to edit them later.
Optimizations
astro:build:donepass converts PNG/JPEG referenced from a CSSurl(...)to WebP and rewrites the reference (blueprints/pluginsbar-bg.png: 1,127 → ~40 KiB); the PNG stays the source of truth. Closes Astro's CSS-background blind spot (CSS background images are a blind spot in Astro's image + CSP story withastro/roadmap#1333).banners.tsroutes each banner throughgetImage()(async, awaited in page frontmatter for SSR safety) instead of shipping the full-size PNG via a bare.src.background-imageJPEG now goes throughgetImage()instead of an unoptimized bare.src./orchestration/*no longer loads the ~1.1 MB (5.6 MB raw) vue-flow topology bundle on page load; a 4 KiBLazyTopologyshell dynamically imports it only when the panel scrolls near the viewport (IntersectionObserver, 200px margin).<iframe>(~460 KiB player) becomes a click-to-play facade; nothing from youtube.com loads until the visitor clicks, with a<noscript>iframe fallback.Safety & scope
url()-gated, so OG/social images (in<meta>, neverurl()) stay PNG/JPEG — Slack/LinkedIn/X don't reliably render WebP.Verification
oxlintclean;astro check0 errors / 0 warnings / 0 hints (423 files).bar-bg→ WebP with no dangling references; theLazyTopologychunk is 4 KiB and dynamically imports the 5.6 MBTopology.clientchunk.api.kestra.io, which the sandbox network policy blocks); CI reaches it and builds end-to-end — see the Lighthouse Benchmark on this PR.🤖 Generated with Claude Code