Skip to content

perf: Core Web Vitals — WebP backgrounds, deferred topology, lazy video embeds - #5216

Open
elevatebart wants to merge 5 commits into
mainfrom
claude/optimize-css-background-images-webp
Open

perf: Core Web Vitals — WebP backgrounds, deferred topology, lazy video embeds#5216
elevatebart wants to merge 5 commits into
mainfrom
claude/optimize-css-background-images-webp

Conversation

@elevatebart

@elevatebart elevatebart commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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

  • CSS background images → WebP at build — an astro:build:done pass converts PNG/JPEG referenced from a CSS url(...) to WebP and rewrites the reference (blueprints/plugins bar-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).
  • Blueprint category banners → WebPbanners.ts routes each banner through getImage() (async, awaited in page frontmatter for SSR safety) instead of shipping the full-size PNG via a bare .src.
  • Airflow whitepaper hero → WebP — the hero's background-image JPEG now goes through getImage() instead of an unoptimized bare .src.
  • Orchestration topology deferred/orchestration/* no longer loads the ~1.1 MB (5.6 MB raw) vue-flow topology bundle on page load; a 4 KiB LazyTopology shell dynamically imports it only when the panel scrolls near the viewport (IntersectionObserver, 200px margin).
  • Quickstart video lazy-loaded — the YouTube <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

  • WebP conversion is url()-gated, so OG/social images (in <meta>, never url()) stay PNG/JPEG — Slack/LinkedIn/X don't reliably render WebP.
  • The topology and video changes are behavior-preserving — same rendering, only the load is deferred.

Verification

  • oxlint clean; astro check 0 errors / 0 warnings / 0 hints (423 files).
  • Confirmed in the build output: bar-bg → WebP with no dangling references; the LazyTopology chunk is 4 KiB and dynamically imports the 5.6 MB Topology.client chunk.
  • The full build can't complete in the authoring sandbox (prerendering calls 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

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
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

☁️ Cloudflare Worker Preview Deployed!

🔗 https://ks-claude-optimize-css-background-docs.kestra-io.workers.dev
🔗 https://7164046f-docs.kestra-io.workers.dev

🔦 Lighthouse Benchmark

Tested: http://localhost:8787 on 2026-07-22 13:01 UTC
Compared against main baseline from 2026-07-22

Scores (0–100, higher is better)

Page Performance Accessibility Best Practices SEO
Home 71 84 54 92
Pricing 98 92 54 100
Enterprise 96 83 54 100
Cloud 94 88 54 100
About Us 84 🔻 -14 91 54 100
Docs Landing 92 88 54 92
Contribute to Kestra (simple docs) 98 88 54 92
Flow (full featured docs) 98 90 54 92
Blog Index 75 🟢 +10 91 54 100
Blog Post (sample) 90 88 54 100
VS Page (sample) 98 88 54 100
Plugins Landing 92 81 54 92
Plugin Page (sample) 94 88 54 100
Plugin Debug Page (sample) 95 88 54 100
Plugin Debug Return Page (sample) 92 88 58 100
Blueprints Landing 81 91 54 100
Blueprint Audit Logs CSV Export 62 92 54 100

Core Web Vitals (lower is better)

Page LCP FCP TBT CLS Speed Index
Home 1.51 s 0.67 s 493 ms 0.000 🟢 1.77 s
Pricing 1.02 s 0.63 s 30 ms 0.000 0.71 s
Enterprise 1.36 s 0.62 s 18 ms 0.003 0.77 s
Cloud 1.58 s 0.55 s 30 ms 0.000 0.79 s
About Us 2.76 s 🔻 0.67 s 20 ms 0.000 0.84 s
Docs Landing 1.75 s 🔻 0.54 s 102 ms 0.000 🟢 0.98 s
Contribute to Kestra (simple docs) 1.08 s 0.61 s 44 ms 🟢 0.000 🟢 0.76 s
Flow (full featured docs) 0.94 s 0.59 s 95 ms 🟢 0.000 1.09 s
Blog Index 8.84 s 0.58 s 33 ms 🔻 0.000 0.99 s 🟢
Blog Post (sample) 2.14 s 🔻 0.53 s 18 ms 0.000 0.73 s
VS Page (sample) 1.04 s 0.59 s 39 ms 🔻 0.000 🟢 0.72 s
Plugins Landing 0.92 s 0.56 s 28 ms 0.000 2.99 s
Plugin Page (sample) 0.92 s 0.59 s 27 ms 0.051 2.21 s
Plugin Debug Page (sample) 0.92 s 0.52 s 70 ms 🔻 0.001 2.04 s
Plugin Debug Return Page (sample) 1.16 s 0.60 s 58 ms 🔻 0.025 2.51 s
Blueprints Landing 1.78 s 1.19 s 77 ms 0.000 4.00 s
Blueprint Audit Logs CSV Export 1.03 s 0.66 s 205 ms 0.483 4.37 s
Legend

🟢 improved  ·  🔻 regressed  ·  (blank) no significant change
Score threshold: ±10 pts  ·  Metric threshold: ±30% of baseline

View full Lighthouse HTML report for a page

Full per-page Lighthouse Results (LHR) are attached as the lhr-reports artifact on this run. Download and unzip it, then open https://googlechrome.github.io/lighthouse/viewer/ and drop the <page>-lhr.json file into the page to see every audit, opportunity, and diagnostic.

claude added 2 commits July 22, 2026 12:19
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
claude added 2 commits July 22, 2026 12:49
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
@elevatebart elevatebart changed the title perf(images): convert CSS background images to WebP at build time perf: Core Web Vitals — WebP backgrounds, deferred topology, lazy video embeds Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants