diff --git a/docusaurus.config.js b/docusaurus.config.js index 0ff78a9b5..4b28e1a3e 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -63,7 +63,16 @@ module.exports = { {property: "og:image:height", content: "630"}, ], headTags: [ - // Google Fonts - DM Sans (loaded via headTags instead of CSS @import) + // ── Preconnect / DNS-prefetch for critical third-party origins ───── + // Keploy CDN + { + tagName: "link", + attributes: { + rel: "preconnect", + href: "https://keploy.io/", + }, + }, + // Google Fonts (used by Docusaurus default theme) { tagName: "link", attributes: { @@ -79,6 +88,7 @@ module.exports = { crossorigin: "anonymous", }, }, + // Google Fonts - DM Sans (loaded via headTags instead of CSS @import) { tagName: "link", attributes: { @@ -86,12 +96,35 @@ module.exports = { href: "https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700&display=swap", }, }, - // Preconnect tag + // Algolia search { tagName: "link", attributes: { rel: "preconnect", - href: "https://keploy.io/", + href: "https://WZTL8PLCOD-dsn.algolia.net", + crossorigin: "anonymous", + }, + }, + // Analytics (dns-prefetch only — not render-blocking) + { + tagName: "link", + attributes: { + rel: "dns-prefetch", + href: "https://www.clarity.ms", + }, + }, + { + tagName: "link", + attributes: { + rel: "dns-prefetch", + href: "https://www.googletagmanager.com", + }, + }, + { + tagName: "link", + attributes: { + rel: "dns-prefetch", + href: "https://www.google-analytics.com", }, }, { diff --git a/netlify.toml b/netlify.toml index 2104fcf94..5e68b2a6b 100644 --- a/netlify.toml +++ b/netlify.toml @@ -9,7 +9,38 @@ ## Note: if you are looking for Redirects # they have been moved to /static/_redirects to make it more manageable - swyx -# Security headers for all pages +# ── Performance: Cache headers ──────────────────────────────────────────────── +# Hashed JS/CSS bundles emitted by Docusaurus/webpack → safe to cache 1 year +[[headers]] + for = "/assets/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +# Static images served from /img +[[headers]] + for = "/img/*" + [headers.values] + Cache-Control = "public, max-age=86400, stale-while-revalidate=604800" + +# Fonts are NOT content-hashed (e.g. Roboto-Bold.woff2) so immutable is unsafe. +# Use a 1-week cache with stale-while-revalidate instead. +[[headers]] + for = "/fonts/*" + [headers.values] + Cache-Control = "public, max-age=604800, stale-while-revalidate=86400" + +# Static JS helpers (non-hashed scripts in /docs/js and /docs/scripts) +[[headers]] + for = "/js/*" + [headers.values] + Cache-Control = "public, max-age=86400" + +[[headers]] + for = "/scripts/*" + [headers.values] + Cache-Control = "public, max-age=86400" + +# ── Security headers (improves PageSpeed Best Practices score) ────────────── [[headers]] for = "/*" [headers.values] diff --git a/src/components/responsive-player/ResponsivePlayer.js b/src/components/responsive-player/ResponsivePlayer.js index d08af04c3..9237b9e2c 100644 --- a/src/components/responsive-player/ResponsivePlayer.js +++ b/src/components/responsive-player/ResponsivePlayer.js @@ -1,5 +1,9 @@ -import React from "react"; -import ReactPlayer from "react-player"; +import React, {Suspense, lazy} from "react"; + +// Lazy-load react-player so it is NOT included in the initial JS bundle. +// react-player/lazy defers loading the actual player implementation until +// the component is rendered, reducing the first-page-load JS payload. +const ReactPlayer = lazy(() => import("react-player/lazy")); function ResponsivePlayer({url, loop, playing}) { return ( @@ -7,16 +11,31 @@ function ResponsivePlayer({url, loop, playing}) { className="relative rounded-lg shadow-lg" style={{paddingTop: "56.25%"}} > - {/* /* Player ratio: 100 / (1280 / 720) */} - + {/* Player ratio: 100 / (1280 / 720) */} + +
+ + Loading video player... + +
+ } + > + +
); } diff --git a/src/css/custom.css b/src/css/custom.css index 739b7f172..115b78b17 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -806,7 +806,8 @@ td img { margin: 0; } -.codeBlockContainer_node_modules-\@docusaurus-theme-classic-lib-next-theme-CodeBlock-styles-module { +[class^="codeBlockContainer_"], +[class*=" codeBlockContainer_"] { box-shadow: none !important; margin: 0; padding: 0; @@ -3202,3 +3203,50 @@ html[data-theme="dark"] .docs-inline-footer__slack { font-size: 0.95rem; } } + +/* ===== PERFORMANCE: Rendering & paint optimisations ===== */ + +/* + * content-visibility: auto — lets the browser skip layout/paint for + * off-screen sections, reducing LCP and INP on mobile. + * contain-intrinsic-size gives the browser a size estimate so the + * scroll-bar doesn't jump when content is rendered. + */ +footer, +.footer { + content-visibility: auto; + contain-intrinsic-size: 0 200px; +} + +/* Sidebar is always below the fold on small viewports */ +@media (max-width: 996px) { + .theme-doc-sidebar-container { + content-visibility: auto; + contain-intrinsic-size: 0 600px; + } +} + +/* + * Lazy-decoded images — any without an explicit loading attribute + * should at minimum decode off the main thread. + */ +img:not([loading]) { + decoding: async; +} + +/* + * Reduce paint layers for the announcement bar (it's a position:sticky + * element and can cause extra compositing cost on mobile). + * Using attribute substring selector to avoid relying on a hashed class name + * that changes between Docusaurus builds. + */ +[class*="announcementBar"] { + will-change: auto; + transform: translateZ(0); +} + +/* + * Font-display: swap is configured per @font-face declaration in + * src/fonts. Docusaurus-injected Google Fonts links already include + * &display=swap in the URL, so no additional override is needed here. + */