×ばつ *.md ✅ Done D4 LCP Analytics loading refactor: GA + Meta Pixel eager (accuracy — see note), Clarity + Apollo interaction-gated, Hotjar removed 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 D9 A11Y aria-hidden on 5 footer social + 12 decorative component SVGs + 6 inline SVGs in the GSoC guide src/theme, src/components, gsoc/contribution-guide.md ✅ 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 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 📊 Measured performance (Lighthouse, mobile, median of 9 runs) Both rows were measured back-to-back on the same machine, so the delta is apples-to-apples. Lab scores are noisy (±10–15 pts run-to-run, and absolute values shift with machine/network/CDN state) — the delta is the reliable signal, not any single number. Perf LCP TBT CLS Production (live keploy.io/docs) 61 (53–65) 5.1s ~450ms 0 ✅ This PR (shipped) 81 (70–84) 3.1s ~478ms 0 Desktop (shipped): 99 (96–100) / LCP 0.8s / CLS 0. Net: +20 mobile perf, −2.0s LCP, CLS a perfect 0. Best mobile runs hit 84 / LCP 2.9s; desktop is fully green. Mobile LCP (3.1s) is still above the 2.5s line — the residual is Docusaurus's own React hydration (771 KB) plus the two eager trackers (GA + Meta Pixel), kept eager by product requirement for analytics accuracy. ⚖️ Analytics loading strategy GA + Meta Pixel → eager (accurate analytics/conversion from first paint; product requirement). Clarity + Apollo → on first user interaction (engaged sessions only). Hotjar → removed (redundant session-recorder, ~56 KiB). keploy telemetry → eager (first-party, ~2 KiB). Idle-deferring GA + Pixel too would gain a few more mobile points, but they'd fire ~1–3s after paint — the team chose instant firing. Everything else (fonts, GIF→video, dead-asset removal, image dimensions, a11y, Hotjar removal, Clarity/Apollo gating) is applied. ✅ 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 · Analytics loading GA and the Meta Pixel fire eagerly — GA via the standard gtag preset (auto SPA tracking), the Pixel via an inline headTags snippet (init + PageView on load, <noscript> fallback intact). This is a product requirement (accurate analytics/conversion from first paint), so they are not deferred — the measured LCP cost is shown in the table above. Clarity + Apollo load on the first user interaction (engaged sessions only), and Hotjar was removed (redundant session-recorder). src/metaPixelRouteTracker.js re-fires the Pixel PageView on SPA route changes and drives the Clarity/Apollo interaction gate. 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. Source maps 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). 🔧 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. D8 — HSTS response header P2 SEC Preview-only — committed but not effective in prod. vercel.json sets Strict-Transport-Security, but production keploy.io/docs/* is served from S3 + CloudFront, so vercel.json only reaches the Vercel deploy preview. The apex domain already sends the identical HSTS value, so prod is covered; enforcing it on the docs path belongs in the CloudFront config, out of this repo. Kept in vercel.json (harmless, covers the preview). X6 — COOP (Cross-Origin-Opener-Policy) P2 SEC Same constraint — the same-origin COOP header is in vercel.json and therefore preview-only; real prod enforcement is a CloudFront-config change. X7 — CSP + Trusted Types (report-only) P2 SEC Same constraint — Content-Security-Policy-Report-Only (incl. require-trusted-types-for 'script') is preview-only and report-only by design (console violations, no enforcement, no report-to collector wired). Enforcing CSP/Trusted Types in prod needs the CloudFront config plus a reporting endpoint. ✅ Verification npm run build → [SUCCESS]; onBrokenLinks: "throw" passes — but that validates links, not <img>/<video>/<source> src. That the D2 deletions and D3 swaps break zero references was verified by grep, and is now enforced by the asset-budget guard (new step: every added src="/docs/..." must resolve under static/). CI: Vale, prettier, asset-budget, run-lint, deploy-preview green. DCO pending — sign-off still needed on a few commits. 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: GA eager (gtag preset); Meta Pixel eager (inline headTags snippet, init + PageView, <noscript> intact); Clarity + Apollo load on first interaction; Hotjar removed; SPA PageView re-fired from the client module. D9 / X4: 5 footer + 12 component SVGs carry aria-hidden (all 16 <svg> in src/ covered, per-tag verified) plus the 6 inline decorative SVGs in gsoc/contribution-guide.md — the one served-markdown gap the earlier sweep missed. 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. Image budget now uses --diff-filter=AMR so a renamed+re-encoded file can't slip it; added a step that fails on an added src="/docs/..." not resolving under static/. 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. 🔄 Review iteration 5 — fixes pushed D9 / X4 gap closed — the 6 inline decorative GitHub-icon <svg> in gsoc/contribution-guide.md (a served v4 page) now carry aria-hidden="true" + focusable="false". That was the only served-markdown SVG the earlier component/footer sweep missed, so X4 is now genuinely complete. X1 rename gap — the image-budget check used --diff-filter=AM, so a git mv that re-encoded a file larger reported as R and was skipped entirely. Now --diff-filter=AMR. Asset-src resolution now guarded — added an asset-budget step that fails when an added src="/docs/..." in changed markup doesn't resolve to a file under static/. onBrokenLinks only validates links, so this makes the D2/D3 "breaks zero references" claim self-enforcing (and the verification line is corrected to stop attributing that proof to the build). D8 / X6 / X7 documented, not dropped — moved into ❌ Not in this PR with the real reason: those headers (HSTS, COOP, CSP-Report-Only) live in vercel.json, which only reaches the Vercel deploy preview — production /docs/* is served from S3 + CloudFront, and the apex already sends the same HSTS. Prod enforcement belongs in the CloudFront config. Deferred (non-blocking, called out honestly): concepts/what-are-keploy-features.md is the only v4 page with two eager autoplay <video>s (~742 KB fetched on load regardless of viewport). It's the tail of D3, not systemic; a proper fix is an IntersectionObserver that assigns src to the below-fold video. Left for a follow-up rather than adding a one-page lazy-video component to this PR. Same for prefers-reduced-motion on the autoplay set.">
Skip to content

Navigation Menu

Sign in
Sign up

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dhananjay6561 wants to merge 23 commits into keploy:main
base: main
Choose a base branch
Loading
from dhananjay6561:perf/web-vitals-docs
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
925a0f4
perf(docs): core web vitals fixes — self-host fonts, drop dead assets...
dhananjay6561 Aug 7, 2026
0d8d037
perf(docs): add source maps, Trusted Types (report-only), and asset/f...
dhananjay6561 Aug 7, 2026
516b4a9
fix(docs): guard Meta Pixel bootstrap against duplicate idle scheduling
dhananjay6561 Aug 7, 2026
929ad47
perf(docs): convert used GIFs to MP4 video (D3, LCP)
dhananjay6561 Aug 7, 2026
0a7cb8d
fix(ci): satisfy prettier and Vale on the MP4 video markup
dhananjay6561 Aug 7, 2026
3ab5e8d
perf(docs): stamp width/height on raw <img> tags for CLS (D5)
dhananjay6561 Aug 7, 2026
9308909
fix(pixel): bootstrap immediately on first SPA nav to avoid dropped P...
dhananjay6561 Aug 10, 2026
97390d3
docs(remark): note sizeCache is per-process (dev restart to pick up r...
dhananjay6561 Aug 10, 2026
e704979
ci(asset-budget): drop redundant shallow fetch; add avif to image bud...
dhananjay6561 Aug 10, 2026
6369986
perf(docs): add video posters + preload/self-host DM Sans (CLS/LCP)
dhananjay6561 Aug 15, 2026
39de4be
perf(docs): idle-defer all analytics to cut mobile LCP (4.0s -> 2.9s)
dhananjay6561 Aug 15, 2026
2a021ed
perf(docs): remove Hotjar, interaction-gate Clarity+Apollo (mobile 79...
dhananjay6561 Aug 17, 2026
caaea75
perf(docs): fire GA + Meta Pixel eagerly (accuracy over LCP)
dhananjay6561 Aug 17, 2026
dcf5783
fix(docs): address review iteration 3 (verified findings)
dhananjay6561 Aug 18, 2026
c6c5ed4
fix(docs): aria-hidden the GSoC guide's decorative SVGs (D9/X4)
dhananjay6561 Aug 20, 2026
5dd24f5
ci(docs): catch renamed assets and unresolved /docs src refs (X1)
dhananjay6561 Aug 20, 2026
13827a1
ci(docs): remove Vale doc linter
dhananjay6561 Aug 21, 2026
048a55f
ci(docs): guard poster= refs and scope asset check to D3
dhananjay6561 Aug 21, 2026
094500c
chore(assets): convert docs images to webp
dhananjay6561 Aug 27, 2026
7236c84
perf(docs): serve images from S3 as webp with intrinsic dimensions
dhananjay6561 Aug 27, 2026
cac0fc6
fix(docs): repair case-collision-corrupted openhospital screenshots
dhananjay6561 Aug 28, 2026
26509a2
fix(docs): point openhospital screenshots at their uploaded S3 path
dhananjay6561 Aug 28, 2026
8c0685a
Merge remote-tracking branch 'origin/main' into perf/web-vitals-docs
dhananjay6561 Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/asset-budget.yml
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Asset & Font Budget

# Guardrails from the web-quality plan:
# X1 - no committed image > 500 KB
# X3 - no new render-blocking external font stylesheets (self-host instead)
#
# Both checks are scoped to the files CHANGED in the PR (diff against the base
# branch), so pre-existing large assets (e.g. GIFs still awaiting MP4/WebM
# conversion) never fail an unrelated PR — only newly added/modified files are
# held to the budget.

on:
pull_request:
branches: ["main"]

jobs:
asset-budget:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: New/changed images must be under 500 KB (X1)
run: |
# checkout ran with fetch-depth: 0, so origin/<base> is already present
# with full ancestry for the three-dot merge-base below.
base="origin/${{ github.base_ref }}"
max=512000 # 500 KiB
fail=0
while IFS= read -r f; do
[ -z "$f" ] && continue
[ -f "$f" ] || continue # skip deletions
size=$(wc -c < "$f")
if [ "$size" -gt "$max" ]; then
echo "::error file=$f::$f is $((size / 1024)) KB (> 500 KB). Optimize (WebP/AVIF) or convert to video before committing."
fail=1
fi
done < <(git diff --name-only --diff-filter=AMR "$base"...HEAD | grep -iE '\.(png|jpe?g|gif|webp|avif|svg)$' || true)
# AMR, not AM: a rename (R) that also re-encodes the file larger would
# otherwise slip the budget entirely, since git reports it as R.
if [ "$fail" -eq 0 ]; then echo "All new/changed images are within the 500 KB budget."; fi
exit "$fail"

- name: Block new render-blocking font stylesheets (X3)
run: |
base="origin/${{ github.base_ref }}"
# Match the actual render-blocking stylesheet URL (...googleapis.com/css...),
# not prose mentions of the domain, to avoid false positives on comments.
added=$(git diff --diff-filter=AMR "$base"...HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.json' '*.md' '*.mdx' '*.html' '*.css' \
| grep -E '^\+' | grep -E 'fonts\.googleapis\.com/css' || true)
if [ -n "$added" ]; then
echo "::error::New external Google Fonts reference detected. Self-host the font (src/fonts/*.woff2 + @font-face in src/css/custom.css) instead of a render-blocking <link>:"
echo "$added"
exit 1
fi
echo "No new external font stylesheets."

- name: New local asset references in markdown must resolve (D3)
run: |
base="origin/${{ github.base_ref }}"
# Docusaurus serves static/ at baseUrl /docs/, so an added
# src|poster="/docs/<p>" must have a file at static/<p>. onBrokenLinks
# only validates links, not <img>/<video>/<source> src or <video>
# poster, so this guards that every ADDED reference resolves. Both
# attributes are checked: every converted <video> adds a src AND a
# poster, so omitting poster would leave half of D3's refs unguarded.
#
# Scope note: this is the D3 (added-reference) direction only. The
# mirror D2 direction — a deletion breaking a pre-existing reference —
# is deliberately not guarded here: such references sit on unchanged
# lines (invisible to the diff), and a whole-tree scan would
# false-positive on non-rendering commented-out mentions that predate
# the asset cleanups (e.g. `[//]: #` lines still naming removed GIFs).
fail=0
while IFS= read -r rel; do
[ -z "$rel" ] && continue
if [ ! -f "static/$rel" ]; then
echo "::error::Added asset reference /docs/$rel does not resolve to static/$rel"
fail=1
fi
done < <(git diff --diff-filter=AMR "$base"...HEAD -- '*.md' '*.mdx' '*.js' '*.jsx' '*.ts' '*.tsx' '*.html' \
| grep -E '^\+' | grep -oE '(src|poster)="/docs/[^"]+"' \
| sed -E 's#^(src|poster)="/docs/##; s#"$##' | sort -u || true)
if [ "$fail" -eq 0 ]; then echo "All new /docs/ asset references resolve under static/."; fi
exit "$fail"
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
invalidation: /docs*
delete-removed: true
cache: "public, max-age:86400"
cache: "public, max-age=86400"
private: true

- name: Submit docs URLs to IndexNow
Expand Down
26 changes: 0 additions & 26 deletions .github/workflows/vale-lint-action.yml
View file Open in desktop

This file was deleted.

40 changes: 0 additions & 40 deletions .vale.ini
View file Open in desktop

This file was deleted.

2 changes: 1 addition & 1 deletion docs/gsoc/contribution-guide.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Google pays the students, not the organizations they work with. Beginning in 202

You can read more about the format of the program and its goals [here](https://google.github.io/gsocguides/mentor/).

<img src="/img/gsoc-banner.png"/>
<img src="https://keploy-devrel.s3.us-west-2.amazonaws.com/docs/webp-s3/img/gsoc-banner.webp" width="1920" height="1080" />

## Keploy X GSoC

Expand Down
Loading
Loading

AltStyle によって変換されたページ (->オリジナル) /