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) {
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) {
e.stopPropagation()}
{...props}
/>