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
Open
perf(docs): core web vitals — self-host fonts, drop dead assets, defer pixel, add security headers#896dhananjay6561 wants to merge 6 commits into
dhananjay6561 wants to merge 6 commits into
Conversation
Contributor
There was a problem hiding this comment.
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-hiddento 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.
…, 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>
dhananjay6561
force-pushed
the
perf/web-vitals-docs
branch
from
August 7, 2026 07:15
0b67f46 to
6b3b894
Compare
Docusaurus's mdx-loader already resolves and sizes Markdown images
(`` 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>
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.
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.
📊 Ticket status
Every docs ticket from the action plan, verified on the served version (v4.0.0 —
CURRENT_DOCS_VERSION; v3.0.0 is not built).Fileverified on this branch.LCPlatin+latin-ext); drop render-blocking Google Fonts<link>src/fonts/DMSans-*.woff2,src/css/custom.css,docusaurus.config.jsLCPDEADstatic/{gif,img,cms}LCP<video>static/gif,static/img, 24×*.mdLCPrequestIdleCallback+ one-shot guardsrc/metaPixelRouteTracker.js,docusaurus.config.jsCLSwidth/heighton raw<img>(dependency-free remark plugin)src/remark/remarkImageSize.js,docusaurus.config.jsLCPdocusaurus.config.jsSECvercel.json: HSTS + COOP + CSP Report-Onlyvercel.jsonA11Yaria-hiddenon 5 footer social + 12 decorative component SVGssrc/theme,src/componentsDEAD"Aeonik"font-familysrc/css/custom.cssSECdevtool: source-map, client-only)docusaurus.config.jsPROC.github/workflows/asset-budget.ymlPROCfonts.googleapis.com/csslink.github/workflows/asset-budget.ymlSECvercel.json(part of D8)SECrequire-trusted-types-for 'script', report-only)vercel.json(part of D8)A11YSEOSEO✅ Changes
Performance — LCP
D1 · Self-host DM Sans
DM Sans loaded via a render-blocking
<link rel="stylesheet">tofonts.googleapis.cominheadTags. Now self-hosted as a variable woff2 (latin+latin-extsubsets) insrc/fonts/, wired via@font-faceincustom.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 rmof 6 assets confirmed 0-ref (grepped repo-wide first):static/gif/unit-test.gifstatic/img/unit-test.gifstatic/img/record-testcase.gifstatic/gif/interoperability.gifstatic/gif/tc-generation.gifstatic/cms/reactor.pngD3 · 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:gif/record-replaygif/replay-tcgif/how-keploy-worksimg/record-apiAll 30 live references across 24 files (v1/v2/v3/v4) were swapped from
<img>/![]()to a<video autoPlay loop muted playsInline>element. Each carries explicitwidth/height(so the browser reserves layout space — a CLS win alongside the LCP one) plus anaria-labeldescribing 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.D4 · Defer Meta Pixel
The pixel bootstrap (
fbevents.js+init+PageView) ran synchronously inheadTags, blocking head parse on every page. Moved into the existingsrc/metaPixelRouteTracker.jsclient module and loaded lazily viarequestIdleCallback(with asetTimeoutfallback 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/heighton raw<img>tagsDocusaurus's mdx-loader already resolves and sizes Markdown images (
renders withwidth/height+ a content-hashed asset), but it leaves hand-written HTML<img>tags in.md/.mdxuntouched — 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 intrinsicwidth/height, giving the browser an aspect ratio to reserve space. The globalimg { 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.lockandpackage-lock.json(Vercel uses yarn, CI uses npm), soimage-size/rehype-img-sizewould 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 againstsipsacross 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/.mdxsource 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 aContent-Security-Policy-Report-Onlycovering all third parties (GTM, GA, Meta Pixel, Clarity, Apollo, Algolia, telemetry) includingrequire-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.
D11 · Client source maps
A
configureWebpackplugin setsdevtool: 'source-map'for the client bundle only — no new dependency, emits.mapfiles 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 carriesaria-label) and 12 decorative component icons.DocItemtheme 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: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
report-tocollector should be wired before enforcing (no fake endpoint added).bootstrapScheduledflag.🔧 Keeping the linters happy
The D3
<video>markup tripped two checks; both are fixed in this PR:<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.autoPlayandplaysInlineas 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)
SEOSoftwareApplicationJSON-LD block (the correct schema.org type for a dev tool; a separateProducttype would be redundant/conflicting). The "review" half is deliberately omitted:aggregateRating/Reviewmarkup 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 — viasameAslinks 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).gstatic/googleapispreconnect; 4 preconnects total.[//]: #markdown comments that predate this PR); 30<video>elements across 24 files, each withwidth,height, andaria-label; MP4s total 1.53 MiB.sipson 188/188 raster assets; built HTML shows 30 raw<img>with injectedwidth/height, 69 author-sized tags untouched, Markdown images unaffected, 0 duplicate attributes; disabling the plugin drops the attributes (isolation confirmed).fbevents.jsin<head>; loader bundled;<noscript>intact;requestIdleCallback+ one-shot guard present.vercel.jsonvalid JSON; HSTS + COOP + CSP-Report-Only present; CSP not enforced; Trusted Types directive present.aria-hidden(per-tag verified).Aeonikfont-family remains..js.mapfiles emitted.robots.txt,sitemap.xml, canonical, JSON-LD all present/valid.prettier --checkclean on all changed files under both 2.8.8 and 3.9.6; Vale clean on changed lines; noyarn.lockchurn.