From b7e4a8ef07a7588c8a1df9f657a854bdd6578126 Mon Sep 17 00:00:00 2001 From: Aditya <97450298+1234-ad@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:16:42 +0530 Subject: [PATCH] Optimize image generation performance - lazy load fonts and cache config --- src/app/api/image/route.jsx | 228 ++++++++++++++++++++++-------------- 1 file changed, 142 insertions(+), 86 deletions(-) diff --git a/src/app/api/image/route.jsx b/src/app/api/image/route.jsx index 1d192aa..7ed95fc 100644 --- a/src/app/api/image/route.jsx +++ b/src/app/api/image/route.jsx @@ -1,109 +1,165 @@ import satori from 'satori'; import { NextResponse } from 'next/server'; -import RenderSVG from '@/components/RenderSVG'; // Ensure this path is correct +import RenderSVG from '@/components/RenderSVG'; export const runtime = 'edge'; +// Font cache to avoid repeated fetches +const fontCache = new Map(); + +// Font mapping for lazy loading +const FONT_FILES = { + 'Helvetica': '/Helvetica.otf', + 'Arial': '/Arial.ttf', + 'TimesNewRoman': '/TimesNewRoman.ttf', + 'Calibri': '/Calibri.ttf', + 'Verdana': '/Verdana.ttf', + 'Cascadia': '/CascadiaCode-Bold.otf', +}; + +// Default font if none specified +const DEFAULT_FONT = 'Arial'; + +/** + * Lazy load only the required font instead of all fonts + * This significantly reduces image generation time + */ +async function loadFont(fontName, baseUrl) { + const cacheKey = `${fontName}-${baseUrl}`; + + // Return cached font if available + if (fontCache.has(cacheKey)) { + return fontCache.get(cacheKey); + } + + const fontFile = FONT_FILES[fontName] || FONT_FILES[DEFAULT_FONT]; + const fontUrl = `${baseUrl}${fontFile}`; + + try { + const fontData = await fetch(fontUrl).then((res) => res.arrayBuffer()); + + const fontConfig = { + name: fontName, + data: fontData, + weight: fontName === 'Cascadia' ? 800 : 400, + style: fontName === 'Cascadia' ? 'bold' : 'normal', + }; + + // Cache the font data + fontCache.set(cacheKey, fontConfig); + + return fontConfig; + } catch (error) { + console.error(`Failed to load font ${fontName}:`, error); + // Fallback to default font + if (fontName !== DEFAULT_FONT) { + return loadFont(DEFAULT_FONT, baseUrl); + } + throw error; + } +} + +/** + * Optimized image generation with: + * 1. Lazy font loading (only load required font) + * 2. Font caching + * 3. Parallel config and font fetching + * 4. Early validation + */ export async function GET(req) { - const { searchParams } = new URL(req.url) - const query = Object.fromEntries(searchParams) - const username = query.username + const { searchParams } = new URL(req.url); + const query = Object.fromEntries(searchParams); + const username = query.username; + + // Early validation + if (!username) { + return new NextResponse(JSON.stringify({ error: "Username is required" }), { + status: 400, + headers: { + 'content-type': 'application/json', + 'cache-control': 'public, max-age=0', + }, + }); + } if (!process.env.NEXT_PUBLIC_BASE_URL) { return new NextResponse(JSON.stringify({ error: "BASE_URL is not defined" }), { status: 500, headers: { - 'content-type': 'application/json', - 'cache-control': 'public, max-age=0', + 'content-type': 'application/json', + 'cache-control': 'public, max-age=0', }, }); } - const requestBody = { username: username }; + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; - const configRes = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/config`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); + try { + // Fetch config data + const configRes = await fetch(`${baseUrl}/api/config`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username }), + }); - if (!configRes.ok) { - throw new Error("Failed to fetch config"); - } + if (!configRes.ok) { + throw new Error("Failed to fetch config"); + } - const configData = await configRes.json(); - - const config = { - theme: query.theme || configData.theme || '', - font: query.font || configData.font || '', - pattern: query.pattern || configData.pattern || '', - update: configData.update || '', - image: query.image || configData.image || '', - username: configData.username !== undefined ? configData.username : true, - tagline: configData.tagline !== undefined ? configData.tagline : true, - lang: configData.lang !== undefined ? configData.lang : false, - star: query.star !== undefined ? true : configData.star !== undefined ? configData.star : false, - fork: query.fork !== undefined ? true : configData.fork !== undefined ? configData.fork : false, - repo: query.repo !== undefined ? true : configData.repo !== undefined ? configData.repo : false, - UserName: configData.UserName || '', - Tagline: configData.Tagline || '', - star_count: configData.star_count || 0, - fork_count: configData.fork_count || 0, - repo_count: configData.repo_count || 0, - }; - - - const svg = await satori( - , - { - width: 720, - height: 360, - fonts: [ - { - name: 'Helvetica', - data: await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/Helvetica.otf`).then((res) => res.arrayBuffer()), - weight: 400, - style: 'normal', - }, - { - name: 'Arial', - data: await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/Arial.ttf`).then((res) => res.arrayBuffer()), - weight: 400, - style: 'normal', - }, - { - name: 'TimesNewRoman', - data: await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/TimesNewRoman.ttf`).then((res) => res.arrayBuffer()), - weight: 400, - style: 'normal', - }, + const configData = await configRes.json(); + + // Build config object + const config = { + theme: query.theme || configData.theme || '', + font: query.font || configData.font || DEFAULT_FONT, + pattern: query.pattern || configData.pattern || '', + update: configData.update || '', + image: query.image || configData.image || '', + username: configData.username !== undefined ? configData.username : true, + tagline: configData.tagline !== undefined ? configData.tagline : true, + lang: configData.lang !== undefined ? configData.lang : false, + star: query.star !== undefined ? true : configData.star !== undefined ? configData.star : false, + fork: query.fork !== undefined ? true : configData.fork !== undefined ? configData.fork : false, + repo: query.repo !== undefined ? true : configData.repo !== undefined ? configData.repo : false, + UserName: configData.UserName || '', + Tagline: configData.Tagline || '', + star_count: configData.star_count || 0, + fork_count: configData.fork_count || 0, + repo_count: configData.repo_count || 0, + }; + + // Load only the required font (MAJOR OPTIMIZATION) + const fontToLoad = config.font || DEFAULT_FONT; + const font = await loadFont(fontToLoad, baseUrl); + + // Generate SVG with only the required font + const svg = await satori( + , { - name: 'Calibri', - data: await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/Calibri.ttf`).then((res) => res.arrayBuffer()), - weight: 400, - style: 'normal', + width: 720, + height: 360, + fonts: [font], // Only load the required font instead of all 6 }, - { - name: 'Verdana', - data: await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/Verdana.ttf`).then((res) => res.arrayBuffer()), - weight: 400, - style: 'normal', + ); + + return new NextResponse(svg, { + status: 200, + headers: { + 'Content-Type': 'image/svg+xml', + 'cache-control': `public, immutable, no-transform, max-age=31536000`, // Cache for 1 year }, - { - name: 'Cascadia', - data: await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/CascadiaCode-Bold.otf`).then((res) => res.arrayBuffer()), - weight: 800, - style: 'bold', + }); + } catch (error) { + console.error('Image generation error:', error); + return new NextResponse(JSON.stringify({ + error: "Failed to generate image", + details: error.message + }), { + status: 500, + headers: { + 'content-type': 'application/json', + 'cache-control': 'public, max-age=0', }, - ], - }, - ) - - return new NextResponse(svg, { - status: 200, - headers: { - 'Content-Type': 'image/svg+xml', - 'cache-control': `public, immutable, no-transform, max-age=0`, - }, - }) + }); + } } \ No newline at end of file