Skip to content

perf(docs): core web vitals — self-host fonts, drop dead assets, defer pixel, add security headers - #896

Open
dhananjay6561 wants to merge 6 commits into
keploy:mainfrom
dhananjay6561:perf/web-vitals-docs
Open

perf(docs): core web vitals — self-host fonts, drop dead assets, defer pixel, add security headers#896
dhananjay6561 wants to merge 6 commits into
keploy:mainfrom
dhananjay6561:perf/web-vitals-docs

Conversation

@dhananjay6561

@dhananjay6561 dhananjay6561 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Core Web Vitals + web-quality fixes for the docs site, from the Aug 2026 web-quality action plan (docs tickets D1–D11 and the docs-relevant cross-cutting X/H items).

Docs mobile was failing LCP (~2.6s) mainly on render-blocking web fonts and heavy GIFs, plus a synchronous third-party pixel and missing security headers. This PR lands every ticket that can be done without a new runtime dependency and without breaking anything — all build-verified. Genuinely-blocked tickets (and why) are listed at the bottom so nothing is silently dropped.

Scope: docs repo only. All landing (L*) and blog (B*) tickets live in their own repos and are out of scope here.


📊 Ticket status

Every docs ticket from the action plan, verified on the served version (v4.0.0CURRENT_DOCS_VERSION; v3.0.0 is not built). File verified on this branch.

ID Area What Key file(s) Status
D1 LCP Self-host DM Sans (variable woff2, latin+latin-ext); drop render-blocking Google Fonts <link> src/fonts/DMSans-*.woff2, src/css/custom.css, docusaurus.config.js ✅ Done
D2 LCP DEAD Delete 6 zero-ref heavy assets (−62 MB) static/{gif,img,cms} ✅ Done
D3 LCP Convert 4 in-use GIFs → H.264 MP4 (−78%); swap 30 refs across 24 files to <video> static/gif, static/img, 24× *.md ✅ Done
D4 LCP Defer Meta Pixel via requestIdleCallback + one-shot guard src/metaPixelRouteTracker.js, docusaurus.config.js ✅ Done
D5 CLS Stamp intrinsic width/height on raw <img> (dependency-free remark plugin) src/remark/remarkImageSize.js, docusaurus.config.js ✅ Done
D6 LCP Preconnects 6 → 4 (follows from D1) docusaurus.config.js ✅ Done
D8 SEC vercel.json: HSTS + COOP + CSP Report-Only vercel.json ✅ Done
D9 A11Y aria-hidden on 5 footer social + 12 decorative component SVGs src/theme, src/components ✅ Done
D10 DEAD Remove undefined "Aeonik" font-family src/css/custom.css ✅ Done
D11 SEC Client source maps (devtool: source-map, client-only) docusaurus.config.js ✅ Done
X1 PROC CI guard — fail on new image > 500 KB .github/workflows/asset-budget.yml ✅ Done
X3 PROC CI guard — fail on new fonts.googleapis.com/css link .github/workflows/asset-budget.yml ✅ Done
X6 SEC COOP header vercel.json (part of D8) ✅ Done
X7 SEC Trusted Types (require-trusted-types-for 'script', report-only) vercel.json (part of D8) ✅ Done
X4 A11Y Accessibility-tree well-formedness for AI crawlers resolved by D9 ✅ Done
H1 SEO robots.txt / sitemap / canonical / JSON-LD present & valid ✅ Verified, no change
H2 SEO Product / review schema ⛔ Won't do — see below

✅ Changes

Performance — LCP

D1 · Self-host DM Sans
DM Sans loaded via a render-blocking <link rel="stylesheet"> to fonts.googleapis.com in headTags. Now self-hosted as a variable woff2 (latin + latin-ext subsets) in src/fonts/, wired via @font-face in custom.css — mirroring the existing Roboto setup (font-display: swap). It's the same font Google serves modern browsers (DM Sans v17 variable), so letterforms are identical; only the source changes.

D6 · Fewer preconnects
Dropped the two now-unused font preconnects. Preconnects 6 → 4 (algolia, keploy.io, GA, GTM) — clears the ">4 preconnect" warning.

D2 · Delete dead heavy assets (~62 MB)
git rm of 6 assets confirmed 0-ref (grepped repo-wide first):

File Size
static/gif/unit-test.gif 24.8 MB
static/img/unit-test.gif 24.8 MB
static/img/record-testcase.gif 6.0 MB
static/gif/interoperability.gif 2.4 MB
static/gif/tc-generation.gif 1.4 MB
static/cms/reactor.png 2.7 MB

D3 · Convert heavy in-use GIFs to H.264 MP4 (7.44 MB → 1.60 MB, −78%)
The four referenced GIFs over 1 MB were the largest remaining LCP/bandwidth cost. Each was re-encoded to H.264 MP4 with faststart (recipe R3) and the GIF deleted:

Asset GIF MP4 Saved
gif/record-replay 2.66 MB 0.28 MB −89%
gif/replay-tc 2.19 MB 0.41 MB −81%
gif/how-keploy-works 1.45 MB 0.14 MB −90%
img/record-api 1.14 MB 0.76 MB −33%
ffmpeg -y -i in.gif -movflags faststart -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -an out.mp4

All 30 live references across 24 files (v1/v2/v3/v4) were swapped from <img>/![]() to a <video autoPlay loop muted playsInline> element. Each carries explicit width/height (so the browser reserves layout space — a CLS win alongside the LCP one) plus an aria-label describing the clip, preserving the alt text the GIFs had. Autoplaying muted inline video keeps the existing "animated screenshot" behaviour on both desktop and mobile Safari.

static/gif/record-tc.gif (278 KB, 11 references) is intentionally left as a GIF — it's an order of magnitude smaller than the four above, so the conversion churn isn't worth it in this PR. Noted as an optional follow-up.

D4 · Defer Meta Pixel
The pixel bootstrap (fbevents.js + init + PageView) ran synchronously in headTags, blocking head parse on every page. Moved into the existing src/metaPixelRouteTracker.js client module and loaded lazily via requestIdleCallback (with a setTimeout fallback for Safari). SPA route-change PageView tracking is preserved; the <noscript> fallback stays inline. Scheduling is guarded by a one-shot flag so rapid SPA navigations can't queue duplicate idle callbacks.

Performance — CLS

D5 · Intrinsic width/height on raw <img> tags
Docusaurus's mdx-loader already resolves and sizes Markdown images (![](/img/x.png) renders with width/height + a content-hashed asset), but it leaves hand-written HTML <img> tags in .md/.mdx untouched — and those are the remaining source of layout shift. A new dependency-free remark plugin (src/remark/remarkImageSize.js) stamps each raw <img> with the image's intrinsic width/height, giving the browser an aspect ratio to reserve space. The global img { max-width:100%; height:auto } rule keeps images fully responsive — the attributes only supply the ratio, not a fixed size.

Why no dependency: the repo carries both yarn.lock and package-lock.json (Vercel uses yarn, CI uses npm), so image-size/rehype-img-size would mean keeping two lockfiles in sync. The plugin reads PNG/GIF/JPEG headers itself. The parser is chosen by magic bytes, not extension, so a mislabeled file (this repo has one PNG saved as .jpg) is still sized correctly — verified against sips across all 188 raster assets (188/188 exact match). It never throws, skips remote/relative/data:/webp/svg, and never overwrites author-provided dimensions.

Build-verified: 30 raw <img> gain dimensions, 69 with author-set widths are left alone, Markdown images are unaffected, zero duplicate attributes. No .md/.mdx source files change — sizing happens at build time.

Security — headers & source maps

D8 · Security headers (+ X6 COOP, X7 Trusted Types)
Added to vercel.json: Strict-Transport-Security (2y, includeSubDomains, preload), Cross-Origin-Opener-Policy: same-origin, and a Content-Security-Policy-Report-Only covering all third parties (GTM, GA, Meta Pixel, Clarity, Apollo, Algolia, telemetry) including require-trusted-types-for 'script'.

Why Report-Only: enforcing CSP on a live Docusaurus site with this many third-party scripts risks silently breaking analytics/search if a source is missed. Report-Only surfaces violations without blocking, so the policy can be tuned first, then flipped to enforced.

Reporting note: the policy has no report-to/report-uri directive yet, so violations are only visible in the browser console — they are not aggregated to a collector. A Reporting-Endpoints header + report-to directive should be wired to a collector before enforcing.

D11 · Client source maps
A configureWebpack plugin sets devtool: 'source-map' for the client bundle only — no new dependency, emits .map files for debuggable first-party JS, zero runtime impact (428 maps emitted).

Accessibility

D9 · Decorative SVGs (+ X4)
Added aria-hidden="true" to the 5 footer social icons (their <a> already carries aria-label) and 12 decorative component icons. DocItem theme SVGs already had it.

D10 · Remove dead font reference
Removed the "Aeonik" font-family — referenced for headings but never defined via @font-face, so it always fell through to the system stack.

Process / CI

X1 + X3 · Asset & font budget guard
New .github/workflows/asset-budget.yml, scoped to files changed in the PR:

  • X1 — fails on any newly added/modified image > 500 KB.
  • X3 — fails on any newly added render-blocking Google Fonts stylesheet reference (fonts.googleapis.com/css…), enforcing D1.

PR-scoped by design, so pre-existing large assets never fail an unrelated PR — only new regressions are caught.

H1 · SEO sanity — verified robots.txt, sitemap.xml, canonical links, and JSON-LD are all present/valid; no change needed.


🔎 Review feedback addressed

  • CSP has no reporting endpoint — description corrected to state violations are console-only for now; a report-to collector should be wired before enforcing (no fake endpoint added).
  • Duplicate idle scheduling in the pixel loader — added a one-shot bootstrapScheduled flag.

🔧 Keeping the linters happy

The D3 <video> markup tripped two checks; both are fixed in this PR:

  • prettier reformatted the markdown HTML block because of the indented <source> child. The two versions disagree on how: 2.8.8 wants a blank line after the opening tag (which would split the JSX block), while 3.9.6 wants the child dedented to column 0. The workflow pins 2.8.8 but the action resolves to 3.9.6, so rather than target either, each <video>/<source>/</video> trio is collapsed onto one line — verified clean under both versions.
  • Vale flagged autoPlay and playsInline as misspellings. They're JSX attribute names, not prose, so they're added to the Base vocabulary alongside the other camelCase identifiers already accepted there (borderRadius, containerName, matchLabels, …).

❌ Not in this PR (and why)

Ticket Priority Why
H2 — Product/review schema P3 SEO Intentionally not done. The "product" half is already covered — the site ships a complete SoftwareApplication JSON-LD block (the correct schema.org type for a dev tool; a separate Product type would be redundant/conflicting). The "review" half is deliberately omitted: aggregateRating/Review markup for one's own product on one's own domain violates Google's structured-data policy (self-serving reviews) and can trigger a manual action suppressing all rich results. Real ratings are surfaced the correct way — via sameAs links to G2/Gartner/Capterra/AWS Marketplace in the Organization schema.

✅ Verification

  • npm run build[SUCCESS]; onBrokenLinks: "throw" passes (the D2 deletions and D3 swaps break zero references).
  • All CI checks green: DCO, Vale, prettier, asset-budget, run-lint, deploy-preview.
  • Fonts: both woff2 subsets emitted + referenced by built CSS (no broken URL); no Google Fonts stylesheet; no gstatic/googleapis preconnect; 4 preconnects total.
  • D3: all 4 GIFs deleted with zero remaining live references (the only mentions left are inside non-rendering [//]: # markdown comments that predate this PR); 30 <video> elements across 24 files, each with width, height, and aria-label; MP4s total 1.53 MiB.
  • D5: remark parser output matches sips on 188/188 raster assets; built HTML shows 30 raw <img> with injected width/height, 69 author-sized tags untouched, Markdown images unaffected, 0 duplicate attributes; disabling the plugin drops the attributes (isolation confirmed).
  • D4: no inline fbevents.js in <head>; loader bundled; <noscript> intact; requestIdleCallback + one-shot guard present.
  • D8/X6/X7: vercel.json valid JSON; HSTS + COOP + CSP-Report-Only present; CSP not enforced; Trusted Types directive present.
  • D9: 5 footer + 12 component SVGs carry aria-hidden (per-tag verified).
  • D10: no real Aeonik font-family remains.
  • D11: 428 .js.map files emitted.
  • X1/X3: workflow valid YAML; simulated on this PR's diff → passes; on a real violation → fails as intended.
  • H1: robots.txt, sitemap.xml, canonical, JSON-LD all present/valid.
  • prettier --check clean on all changed files under both 2.8.8 and 3.9.6; Vale clean on changed lines; no yarn.lock churn.

Local preview: npm run serve (production preview) can't serve the fonts due to the repo's trailingSlash: true setting (it 302→404s every woff2 — the pre-existing Roboto font too), so DM Sans falls back to a system font locally. Use npm start (dev server) to preview fonts correctly. On Vercel the self-hosted fonts load fine, exactly like Roboto does today.

Copilot AI lite review requested due to automatic review settings August 7, 2026 05:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the docs site’s Core Web Vitals (notably LCP) and security posture by removing render-blocking third-party resources, self-hosting fonts, deferring non-critical analytics, and tightening response headers. It also includes small accessibility fixes for decorative SVGs.

Changes:

  • Self-host DM Sans (woff2 variable font subsets) and remove Google Fonts preconnect/stylesheet from headTags.
  • Defer Meta Pixel bootstrap to idle time via a Docusaurus client module while preserving SPA PageView tracking and <noscript> fallback.
  • Add security headers on Vercel (HSTS, COOP) and introduce a CSP header in Report-Only mode; add aria-hidden to decorative SVG icons.

Reviewed changes

Copilot reviewed 9 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
vercel.json Adds HSTS, COOP, and CSP Report-Only header configuration.
src/metaPixelRouteTracker.js Implements lazy Meta Pixel loader and SPA PageView tracking on route changes.
src/css/custom.css Adds self-hosted DM Sans @font-face rules and removes unused “Aeonik” font reference.
src/components/WhatIsKeploy.js Marks decorative SVGs as aria-hidden and applies formatting tweaks.
src/components/UtgMethods.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Resources.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Product.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Intro.js Adds aria-hidden="true" to decorative SVG icons.
docusaurus.config.js Removes Google Fonts + synchronous Meta Pixel head injection; keeps noscript fallback and registers client module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vercel.json
Comment thread src/metaPixelRouteTracker.js
@dhananjay6561 dhananjay6561 self-assigned this Aug 7, 2026
…, defer pixel, security headers

- D1/D6: self-host DM Sans as a variable woff2 (latin + latin-ext), mirroring
  the existing Roboto @font-face setup; remove the render-blocking Google Fonts
  stylesheet and its two preconnects from headTags (preconnects 6 -> 4)
- D2: delete 6 confirmed 0-ref heavy assets (2x unit-test.gif, record-testcase,
  interoperability, tc-generation gifs + reactor.png) — ~62 MB
- D4: move Meta Pixel bootstrap out of synchronous headTags into the
  metaPixelRouteTracker client module, loading it lazily via requestIdleCallback
  (keeps the noscript fallback and SPA PageView tracking)
- D8: add HSTS + Cross-Origin-Opener-Policy and a Content-Security-Policy in
  Report-Only mode to vercel.json
- D9: add aria-hidden to decorative footer + component SVG icons
- D10: remove the undefined "Aeonik" font-family reference

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…ont CI guards

- D11: emit client source maps via a configureWebpack plugin (devtool:
  source-map for the client bundle only) — no new dependency, only extra
  .map files, zero runtime impact
- X7: add `require-trusted-types-for 'script'` to the Report-Only CSP so
  DOM-XSS sinks are reported (cannot block — Report-Only)
- X1 + X3: new asset-budget CI workflow, scoped to files CHANGED in the PR,
  that fails on newly added/modified images > 500 KB and on new
  render-blocking Google Fonts stylesheet references. PR-scoped so existing
  large assets (pending GIF->video) never fail unrelated PRs.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Addresses PR review: scheduleBootstrap() could queue multiple
requestIdleCallback/setTimeout tasks on rapid SPA navigations before the
first idle callback fires. Add a module-level flag so we schedule the
bootstrap at most once (bootstrapPixel already no-ops on window.fbq).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
The 4 GIFs that are actually embedded in docs (record-replay, replay-tc,
how-keploy-works, record-api) were 7.1 MB of uncompressed animation and were
typically the LCP element on the pages that use them. Convert each to H.264
MP4 (faststart, yuv420p) and swap all 30 live references (across v1/v2/v3/v4,
24 files) from GIF <img>/markdown to a looping muted autoplay <video>:

  static/gif/record-replay.gif   2.5M -> record-replay.mp4   276K
  static/gif/replay-tc.gif       2.1M -> replay-tc.mp4       408K
  static/gif/how-keploy-works.gif 1.4M -> how-keploy-works.mp4 140K
  static/img/record-api.gif      1.1M -> record-api.mp4      804K
  total                          7.1M -> 1.6M

Each <video> carries intrinsic width/height (aspect-ratio reserved -> no CLS),
autoPlay/loop/muted/playsInline to mimic the GIF, and the old alt text as
aria-label. Paths normalised to the baseUrl-correct /docs/... form. Commented-
out references were left untouched. Build verified (MDX parses the JSX video
blocks; onBrokenLinks: throw passes).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
  The D3 GIF-to-MP4 swap introduced <video> blocks that broke two checks.
  prettier: the indented <source> child made prettier reformat the markdown
  HTML block. The two versions disagree on how — 2.8.8 wants a blank line
  after the opening tag (which would split the JSX block), while 3.9.6 wants
  the child dedented to column 0. The workflow pins 2.8.8 but the action
  installs 3.9.6, so target neither: collapsing each trio onto a single line
  is a fixpoint for both.
  Vale: autoPlay and playsInline are JSX attribute names, not prose, so add
  them to the Base vocabulary alongside the other camelCase identifiers
  already accepted there.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Docusaurus's mdx-loader already resolves and sizes Markdown images
(`![](/img/x.png)` renders with width/height + a hashed asset), but it
leaves hand-written HTML <img> tags in .md/.mdx untouched. Those are the
remaining source of layout shift, so a dependency-free remark plugin now
stamps each raw <img> with the image's intrinsic width/height, giving the
browser an aspect ratio to reserve space (the global
`img { max-width:100%; height:auto }` keeps them responsive).

No dependency (avoids syncing the repo's dual yarn.lock/package-lock.json):
the plugin reads PNG/GIF/JPEG headers itself. The parser is chosen by magic
bytes, not extension, so a mislabeled file (this repo has one PNG saved as
.jpg) is still sized correctly — verified against `sips` on all 188 raster
assets (188/188 exact match). It never throws, skips
remote/relative/data/webp/svg, and never overwrites author dimensions.

Build-verified: 30 raw <img> gain dimensions, 69 with author widths are left
alone, Markdown images are unaffected, zero duplicate attributes. No .md/.mdx
source files change — sizing happens at build time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
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