Skip to content

Commit 7ea31e7

Browse files
perf: improve mobile Core Web Vitals – infra & rendering optimisations
- netlify.toml: add 1-year immutable cache for hashed assets (/assets/*, /fonts/*), 1-day cache for images/scripts, and security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) to improve PageSpeed Best Practices score - docusaurus.config.js: add viewport meta tag with viewport-fit=cover; add preconnect hints for fonts.googleapis.com, fonts.gstatic.com, and Algolia DSN; add dns-prefetch for Clarity, GTM, and GA analytics origins to reduce connection setup latency - src/css/custom.css: add content-visibility:auto + contain-intrinsic-size for footer and mobile sidebar to skip off-screen paint; add decoding:async fallback for un-annotated images; add font-display:swap safety net for any stylesheet-loaded webfonts - ResponsivePlayer.js: lazy-load react-player via React.lazy + React.Suspense (react-player/lazy) so the ~100 KB player bundle is NOT included in the initial JS chunk – reduces TBT/INP on first load Closes #2042 Signed-off-by: Aryan Parikh <aryan81006@gmail.com>
1 parent 6f91d68 commit 7ea31e7

4 files changed

Lines changed: 171 additions & 13 deletions

File tree

docusaurus.config.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,70 @@ module.exports = {
5353
{name: "twitter:card", content: "summary_large_image"},
5454
],
5555
headTags: [
56-
// Preconnect tag
56+
// ── Viewport (mobile performance) ──────────────────────────────────
57+
{
58+
tagName: "meta",
59+
attributes: {
60+
name: "viewport",
61+
content: "width=device-width, initial-scale=1.0, viewport-fit=cover",
62+
},
63+
},
64+
// ── Preconnect / DNS-prefetch for critical third-party origins ─────
65+
// Keploy CDN
5766
{
5867
tagName: "link",
5968
attributes: {
6069
rel: "preconnect",
6170
href: "https://keploy.io/",
6271
},
6372
},
73+
// Google Fonts (used by Docusaurus default theme)
74+
{
75+
tagName: "link",
76+
attributes: {
77+
rel: "preconnect",
78+
href: "https://fonts.googleapis.com",
79+
},
80+
},
81+
{
82+
tagName: "link",
83+
attributes: {
84+
rel: "preconnect",
85+
href: "https://fonts.gstatic.com",
86+
crossorigin: "anonymous",
87+
},
88+
},
89+
// Algolia search
90+
{
91+
tagName: "link",
92+
attributes: {
93+
rel: "preconnect",
94+
href: "https://WZTL8PLCOD-dsn.algolia.net",
95+
crossorigin: "anonymous",
96+
},
97+
},
98+
// Analytics (dns-prefetch only — not render-blocking)
99+
{
100+
tagName: "link",
101+
attributes: {
102+
rel: "dns-prefetch",
103+
href: "https://www.clarity.ms",
104+
},
105+
},
106+
{
107+
tagName: "link",
108+
attributes: {
109+
rel: "dns-prefetch",
110+
href: "https://www.googletagmanager.com",
111+
},
112+
},
113+
{
114+
tagName: "link",
115+
attributes: {
116+
rel: "dns-prefetch",
117+
href: "https://www.google-analytics.com",
118+
},
119+
},
64120
{
65121
tagName: "script",
66122
attributes: {

netlify.toml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,41 @@
88

99
## Note: if you are looking for Redirects
1010
# they have been moved to /static/_redirects to make it more manageable - swyx
11+
12+
# ── Performance: Cache headers ────────────────────────────────────────────────
13+
# Hashed JS/CSS bundles emitted by Docusaurus/webpack → safe to cache 1 year
14+
[[headers]]
15+
for = "/assets/*"
16+
[headers.values]
17+
Cache-Control = "public, max-age=31536000, immutable"
18+
19+
# Static images, fonts served from /img and /fonts
20+
[[headers]]
21+
for = "/img/*"
22+
[headers.values]
23+
Cache-Control = "public, max-age=86400, stale-while-revalidate=604800"
24+
25+
[[headers]]
26+
for = "/fonts/*"
27+
[headers.values]
28+
Cache-Control = "public, max-age=31536000, immutable"
29+
30+
# Static JS helpers (non-hashed scripts in /docs/js and /docs/scripts)
31+
[[headers]]
32+
for = "/js/*"
33+
[headers.values]
34+
Cache-Control = "public, max-age=86400"
35+
36+
[[headers]]
37+
for = "/scripts/*"
38+
[headers.values]
39+
Cache-Control = "public, max-age=86400"
40+
41+
# ── Security headers (improves PageSpeed Best Practices score) ──────────────
42+
[[headers]]
43+
for = "/*"
44+
[headers.values]
45+
X-Frame-Options = "SAMEORIGIN"
46+
X-Content-Type-Options = "nosniff"
47+
Referrer-Policy = "strict-origin-when-cross-origin"
48+
Permissions-Policy = "camera=(), microphone=(), geolocation=()"

src/components/responsive-player/ResponsivePlayer.js

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
1-
import React from "react";
2-
import ReactPlayer from "react-player";
1+
import React, {Suspense, lazy} from "react";
2+
3+
// Lazy-load react-player so it is NOT included in the initial JS bundle.
4+
// react-player/lazy defers loading the actual player implementation until
5+
// the component is rendered, reducing the first-page-load JS payload.
6+
const ReactPlayer = lazy(() => import("react-player/lazy"));
37

48
function ResponsivePlayer({url, loop, playing}) {
59
return (
610
<div
711
className="relative rounded-lg shadow-lg"
812
style={{paddingTop: "56.25%"}}
913
>
10-
{/* /* Player ratio: 100 / (1280 / 720) */}
11-
<ReactPlayer
12-
className="absolute left-0 top-0"
13-
url={url}
14-
loop={loop}
15-
playing={playing}
16-
width="100%"
17-
height="100%"
18-
controls={true}
19-
/>
14+
{/* Player ratio: 100 / (1280 / 720) */}
15+
<Suspense
16+
fallback={
17+
<div
18+
className="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
19+
aria-label="Loading video player"
20+
/>
21+
}
22+
>
23+
<ReactPlayer
24+
className="absolute left-0 top-0"
25+
url={url}
26+
loop={loop}
27+
playing={playing}
28+
width="100%"
29+
height="100%"
30+
controls={true}
31+
/>
32+
</Suspense>
2033
</div>
2134
);
2235
}

src/css/custom.css

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2721,3 +2721,54 @@ html[data-theme="dark"] div[class^="sidebar_"] > nav > ul > li > .menu__list-ite
27212721
line-height: 1.4;
27222722
}
27232723
}
2724+
2725+
/* ===== PERFORMANCE: Rendering & paint optimisations ===== */
2726+
2727+
/*
2728+
* content-visibility: auto — lets the browser skip layout/paint for
2729+
* off-screen sections, reducing LCP and INP on mobile.
2730+
* contain-intrinsic-size gives the browser a size estimate so the
2731+
* scroll-bar doesn't jump when content is rendered.
2732+
*/
2733+
footer,
2734+
.footer {
2735+
content-visibility: auto;
2736+
contain-intrinsic-size: 0 200px;
2737+
}
2738+
2739+
/* Sidebar is always below the fold on small viewports */
2740+
@media (max-width: 996px) {
2741+
.theme-doc-sidebar-container {
2742+
content-visibility: auto;
2743+
contain-intrinsic-size: 0 600px;
2744+
}
2745+
}
2746+
2747+
/*
2748+
* Lazy-decoded images — any <img> without an explicit loading attribute
2749+
* should at minimum decode off the main thread.
2750+
*/
2751+
img:not([loading]) {
2752+
decoding: async;
2753+
}
2754+
2755+
/*
2756+
* Reduce paint layers for the announcement bar (it's a position:sticky
2757+
* element and can cause extra compositing cost on mobile).
2758+
*/
2759+
.announcementBar_mb4j {
2760+
will-change: auto;
2761+
transform: translateZ(0);
2762+
}
2763+
2764+
/*
2765+
* Font-display: swap fallback for any @font-face rules Docusaurus injects.
2766+
* This prevents invisible text during webfont load (FOIT → FOUT).
2767+
* The actual font files are already preloaded via webpack-font-preload-plugin;
2768+
* this rule is a safety net for any font loaded via a stylesheet.
2769+
*/
2770+
@supports (font-display: swap) {
2771+
@font-face {
2772+
font-display: swap;
2773+
}
2774+
}

0 commit comments

Comments
 (0)