From de3236ca517a380a40880a874874822dca9a84a8 Mon Sep 17 00:00:00 2001 From: seferturan Date: Mon, 17 Aug 2026 21:43:08 +0200 Subject: [PATCH 1/3] perf(share): loads share card fonts from R2 instead of a third party CDN sveltekit-og fetches NotoSans regular and bold from cdn-sveltekit-og.ethercorps.io on every cold generation. Sentry has that pair at 6s to 19s across the past week, which is long enough for a social scraper to give up before the card renders, and it puts a third party in the critical path of every share image. Reads both faces from the R2 bucket the endpoint already binds, memoised per isolate. Falls back to the sveltekit-og defaults when the bucket or an object is missing, so dev keeps working and a bad upload degrades instead of breaking. Needs assets/fonts/NotoSans-Regular.ttf and assets/fonts/NotoSans-Bold.ttf uploaded to walter before this has any effect. --- .../src/routes/api/shareable-image/+server.ts | 3 + .../_internal/loadShareFonts.spec.ts | 81 +++++++++++++++++++ .../_internal/loadShareFonts.ts | 54 +++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.spec.ts create mode 100644 projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.ts diff --git a/projects/client/src/routes/api/shareable-image/+server.ts b/projects/client/src/routes/api/shareable-image/+server.ts index 559f016732..75837aabdf 100644 --- a/projects/client/src/routes/api/shareable-image/+server.ts +++ b/projects/client/src/routes/api/shareable-image/+server.ts @@ -11,6 +11,7 @@ import { buildImageMetadata } from './_internal/buildImageMetadata.ts'; import { buildImagePath } from './_internal/buildImagePath.ts'; import { fetchMediaData } from './_internal/fetchMediaData.ts'; import { fetchWithUserAgent } from './_internal/fetchWithUserAgent.ts'; +import { loadShareFonts } from './_internal/loadShareFonts.ts'; import { resolvePosterDataUri } from './_internal/resolvePosterDataUri.ts'; const cacheControl = 'public, max-age=604800'; @@ -85,6 +86,7 @@ export const GET: RequestHandler = async ( }); const { width, height } = SHARE_TYPE_DIMENSIONS[shareType]; + const fonts = await loadShareFonts({ bucket: platform?.env?.R2_WALTER }); try { const imageResponse = new ImageResponse( @@ -92,6 +94,7 @@ export const GET: RequestHandler = async ( { width, height, + fonts, debug: IS_DEV && url.searchParams.get('debug') === 'true', }, { media, crew, ratings, posterUrl: posterDataUri, variant: shareType }, diff --git a/projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.spec.ts b/projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.spec.ts new file mode 100644 index 0000000000..c179fe534b --- /dev/null +++ b/projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.spec.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest'; + +type LoadShareFonts = typeof import('./loadShareFonts.ts')['loadShareFonts']; +type Bucket = Parameters[0]['bucket']; + +const REGULAR = 'assets/fonts/NotoSans-Regular.ttf'; +const BOLD = 'assets/fonts/NotoSans-Bold.ttf'; + +async function freshLoader(): Promise { + vi.resetModules(); + const { loadShareFonts } = await import('./loadShareFonts.ts'); + + return loadShareFonts; +} + +function bucketWith(paths: ReadonlyArray) { + const get = vi.fn((path: string) => { + if (!paths.includes(path)) { + return Promise.resolve(null); + } + + return Promise.resolve({ + arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), + }); + }); + + return { get } as unknown as Bucket & { get: typeof get }; +} + +describe('util: loadShareFonts', () => { + describe('when the bucket has both weights', () => { + it('should return a regular and a bold face', async () => { + const loadShareFonts = await freshLoader(); + + const fonts = await loadShareFonts({ + bucket: bucketWith([REGULAR, BOLD]), + }); + + expect(fonts?.map((font) => font.weight)).toEqual([400, 700]); + }); + + it('should read each face only once across calls', async () => { + const loadShareFonts = await freshLoader(); + const bucket = bucketWith([REGULAR, BOLD]); + + await loadShareFonts({ bucket }); + await loadShareFonts({ bucket }); + + expect(bucket.get).toHaveBeenCalledTimes(2); + }); + }); + + describe('when the fonts are unavailable', () => { + it('should fall back when there is no bucket', async () => { + const loadShareFonts = await freshLoader(); + + const fonts = await loadShareFonts({ bucket: undefined }); + + expect(fonts).toBeUndefined(); + }); + + it('should fall back when a face is missing', async () => { + const loadShareFonts = await freshLoader(); + + const fonts = await loadShareFonts({ bucket: bucketWith([REGULAR]) }); + + expect(fonts).toBeUndefined(); + }); + + it('should retry on the next call rather than caching the failure', async () => { + const loadShareFonts = await freshLoader(); + + await loadShareFonts({ bucket: bucketWith([]) }); + const fonts = await loadShareFonts({ + bucket: bucketWith([REGULAR, BOLD]), + }); + + expect(fonts?.map((font) => font.weight)).toEqual([400, 700]); + }); + }); +}); diff --git a/projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.ts b/projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.ts new file mode 100644 index 0000000000..9dbc041e63 --- /dev/null +++ b/projects/client/src/routes/api/shareable-image/_internal/loadShareFonts.ts @@ -0,0 +1,54 @@ +import { error } from '$lib/utils/console/print.ts'; +import type { R2Bucket } from '@cloudflare/workers-types'; +import type { ImageResponseOptions } from '@ethercorps/sveltekit-og'; + +type ShareFonts = NonNullable; + +type LoadShareFontsProps = { + bucket: Pick | Nil; +}; + +const FONT_FAMILY = 'Inter'; + +const FONT_SOURCES = [ + { path: 'assets/fonts/NotoSans-Regular.ttf', weight: 400 }, + { path: 'assets/fonts/NotoSans-Bold.ttf', weight: 700 }, +] as const; + +let cachedFonts: ShareFonts | undefined; + +export async function loadShareFonts( + { bucket }: LoadShareFontsProps, +): Promise { + if (cachedFonts) { + return cachedFonts; + } + + if (!bucket) { + return; + } + + try { + cachedFonts = await Promise.all( + FONT_SOURCES.map(async ({ path, weight }) => { + const object = await bucket.get(path); + + if (!object) { + throw new Error(`Missing font in R2: ${path}`); + } + + return { + name: FONT_FAMILY, + data: await object.arrayBuffer(), + weight, + style: 'normal', + } as const; + }), + ); + + return cachedFonts; + } catch (e) { + error('Failed to load share fonts, falling back to the bundled set:', e); + return; + } +} From 49f90afce4b81df0336d805ddc1a6c50c1b3550e Mon Sep 17 00:00:00 2001 From: seferturan Date: Tue, 18 Aug 2026 12:21:52 +0200 Subject: [PATCH 2/3] perf(share): collapses the poster shadow to a single layer Satori render time on the poster shadow, averaged over three samples per variant: the five layer stack cost ~783ms on open-graph and ~1413ms on feed, against ~538ms and ~764ms for one layer. That is 31% and 46% off the render. Cost tracks the area the blur covers, not the number of layers, so alpha is free. Keeping it low matters: a single layer at 0.5 alpha concentrates the whole shadow into a visible slab with a hard edge along the poster radius, where five faint layers blended into a smooth falloff. 0.22 over a 32px blur reads like the original without the cost. --- .../client/src/lib/features/share/_internal/Poster.svelte | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/projects/client/src/lib/features/share/_internal/Poster.svelte b/projects/client/src/lib/features/share/_internal/Poster.svelte index 1c94e0059f..9969053499 100644 --- a/projects/client/src/lib/features/share/_internal/Poster.svelte +++ b/projects/client/src/lib/features/share/_internal/Poster.svelte @@ -47,11 +47,6 @@ .trakt-share-poster { border-radius: 12px; - box-shadow: - 0px 3.08px 7px 0px rgba(19, 21, 23, 0.16), - 0px 12.6px 12.6px 0px rgba(19, 21, 23, 0.14), - 0px 28.28px 16.8px 0px rgba(19, 21, 23, 0.08), - 0px 50.12px 20.16px 0px rgba(19, 21, 23, 0.02), - 0px 78.4px 21.84px 0px rgba(19, 21, 23, 0); + box-shadow: 0px 12px 32px 0px rgba(19, 21, 23, 0.22); } From 68d972376e339cf189859d3db201aa37c27892d4 Mon Sep 17 00:00:00 2001 From: seferturan Date: Tue, 18 Aug 2026 11:58:55 +0200 Subject: [PATCH 3/3] perf(share): resolves the poster and the fonts in parallel Both were awaited in sequence even though neither depends on the other, so the request paid for both round trips back to back. Neither can reject, they both degrade internally, so Promise.all does not change the failure behaviour. --- .../client/src/routes/api/shareable-image/+server.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/projects/client/src/routes/api/shareable-image/+server.ts b/projects/client/src/routes/api/shareable-image/+server.ts index 75837aabdf..d75287affa 100644 --- a/projects/client/src/routes/api/shareable-image/+server.ts +++ b/projects/client/src/routes/api/shareable-image/+server.ts @@ -80,13 +80,15 @@ export const GET: RequestHandler = async ( const { media, ratings, crew } = mediaData; - const posterDataUri = await resolvePosterDataUri({ - posterUrl: media.poster.url.medium, - fetch: fetchFn, - }); + const [posterDataUri, fonts] = await Promise.all([ + resolvePosterDataUri({ + posterUrl: media.poster.url.medium, + fetch: fetchFn, + }), + loadShareFonts({ bucket: platform?.env?.R2_WALTER }), + ]); const { width, height } = SHARE_TYPE_DIMENSIONS[shareType]; - const fonts = await loadShareFonts({ bucket: platform?.env?.R2_WALTER }); try { const imageResponse = new ImageResponse(