From 974b0fcb78fee7da3d3cfb384ca18c9628b7cbc7 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:12:35 -0600 Subject: [PATCH 1/2] perf: lazy-load MDX images ZoomableImage renders every doc image eagerly, so a page with several 1 MB PNGs downloads all of them before first paint. --- src/components/mdx/ZoomableImage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/mdx/ZoomableImage.tsx b/src/components/mdx/ZoomableImage.tsx index ef395215c..3605c9e1a 100644 --- a/src/components/mdx/ZoomableImage.tsx +++ b/src/components/mdx/ZoomableImage.tsx @@ -32,6 +32,8 @@ export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) { {alt} From de39caaa0d7ece8c0ff8abebe535fc33dd4fb52a Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:59:45 -0600 Subject: [PATCH 2/2] perf: serve MDX screenshots as WebP through Next's image optimizer The docs embed ~360 PNG screenshots from GCS (sourcegraphstatic.com and storage.googleapis.com/sourcegraph-assets), uncompressed and with a one-hour cache. Route allow-listed PNG/JPEG sources through /_next/image, which converts to WebP, caps width at 1920px and caches at Vercel's edge for a week. The zoom modal keeps the original full-resolution file. Amp-Thread-ID: https://ampcode.com/threads/T-01a0ac9d-704f-73fe-8211-1b3e5840969f Co-authored-by: Amp --- docs.config.js | 8 +++++++- next.config.js | 7 +++++++ src/components/mdx/ZoomableImage.tsx | 18 +++++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs.config.js b/docs.config.js index 5fec13fd4..7a40b6fae 100644 --- a/docs.config.js +++ b/docs.config.js @@ -1,5 +1,11 @@ const config = { - DOCS_LATEST_VERSION: '7.7' + DOCS_LATEST_VERSION: '7.7', + // Remote image origins that Next's optimizer may fetch, convert to WebP and cache. + OPTIMIZED_IMAGE_ORIGINS: [ + 'https://sourcegraphstatic.com/', + 'https://storage.googleapis.com/sourcegraph-assets/', + 'https://storage.googleapis.com/changelog-static-assets-prod/' + ] }; module.exports = config; diff --git a/next.config.js b/next.config.js index 2603ca187..5024f0ea1 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,5 @@ const {withContentlayer} = require('next-contentlayer2'); +const {OPTIMIZED_IMAGE_ORIGINS} = require('./docs.config'); /** @type {import('next').NextConfig} */ // in prod, we serve the docs from sourcegraph.com/docs, and this requires special config on the GFE side @@ -18,6 +19,12 @@ const nextConfig = { env: { NEXT_PUBLIC_DOCS_BASE_PATH: basePath }, + // MDX screenshots are uncompressed PNGs on GCS with a one-hour cache. ZoomableImage + // routes them through `/_next/image`, which serves WebP from Vercel's edge cache. + images: { + remotePatterns: OPTIMIZED_IMAGE_ORIGINS.map(origin => new URL(`${origin}**`)), + minimumCacheTTL: 60 * 60 * 24 * 7 + }, // With basePath set, nothing serves `/`, so the *.vercel.app deployment URL // that Vercel links from Slack / GitHub 404s. sourcegraph.com never proxies // `/` to us, so this only affects visitors opening that raw URL. Stay on the diff --git a/src/components/mdx/ZoomableImage.tsx b/src/components/mdx/ZoomableImage.tsx index 3605c9e1a..daba9fca6 100644 --- a/src/components/mdx/ZoomableImage.tsx +++ b/src/components/mdx/ZoomableImage.tsx @@ -2,9 +2,23 @@ import {useCallback, useEffect, useState} from 'react'; +import docsConfig from '../../../docs.config'; + interface ZoomableImageProps extends React.ImgHTMLAttributes {} -export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) { +/** + * Serve allow-listed remote PNG/JPEGs through Next's image optimizer: WebP, at most + * 1920px wide (enough for the content column on a 2x display), cached at the edge. + * The width must be one of Next's default `deviceSizes`. + */ +function optimizedSrc(src: ZoomableImageProps['src']) { + if (typeof src !== 'string' || !/\.(png|jpe?g)$/i.test(src)) return src; + if (!docsConfig.OPTIMIZED_IMAGE_ORIGINS.some(origin => src.startsWith(origin))) return src; + const basePath = process.env.NEXT_PUBLIC_DOCS_BASE_PATH ?? ''; + return `${basePath}/_next/image?url=${encodeURIComponent(src)}&w=1920&q=75`; +} + +export function ZoomableImage({className, alt, src, ...props}: ZoomableImageProps) { const [isOpen, setIsOpen] = useState(false); const openModal = useCallback(() => setIsOpen(true), []); @@ -32,6 +46,7 @@ export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) { {alt} e.stopPropagation()} {...props} />