perf(frontend): lazy-register CJK font in PDF generator

Skips loading NotoSansSC.otf (8.1 MB) for reports without CJK chars.
Measured win on flow #50 download: 22.6 MB → 13.8 MB total traffic.
Adds `u` flag to CJK regexes so emoji surrogate pairs don't trigger
false positives (was matching U+D83D high surrogate on flow #41).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-05-24 09:55:44 +07:00
co-authored by Claude Opus 4.7
parent adddb45158
commit 02ef55cb30
+57 -30
View File
@@ -3,40 +3,63 @@ import { marked } from 'marked';
import { Log } from '@/lib/log';
// Register Noto Sans (covers Latin + Cyrillic + Greek + many other scripts)
Font.register({
family: 'NotoSans',
fonts: [
{ fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSans-Regular.ttf' },
{ fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSans-Bold.ttf' },
{ fontStyle: 'italic', fontWeight: 'normal', src: '/fonts/NotoSans-Italic.ttf' },
{ fontStyle: 'italic', fontWeight: 'bold', src: '/fonts/NotoSans-BoldItalic.ttf' },
],
});
const CJK_RE = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3000-\u303F\uFF01-\uFF60\uFFE0-\uFFE6]+/gu;
const HAS_CJK_RE = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3000-\u303F\uFF01-\uFF60\uFFE0-\uFFE6]/u;
// Register Noto Sans Mono (covers Latin + Cyrillic for code blocks)
Font.register({
family: 'NotoSansMono',
fonts: [
{ fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansMono-Regular.ttf' },
{ fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansMono-Bold.ttf' },
],
});
let hasBaseFonts = false;
let hasCJKFonts = false;
// Register Noto Sans SC (Simplified Chinese, covers CJK + Latin)
Font.register({
family: 'NotoSansSC',
fonts: [
{ fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansSC-Regular.otf' },
{ fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansSC-Bold.otf' },
],
});
const registerBaseFonts = (): void => {
if (hasBaseFonts) {
return;
}
// Disable word hyphenation (breaks CJK and Cyrillic incorrectly)
Font.registerHyphenationCallback((word) => [word]);
Font.register({
family: 'NotoSans',
fonts: [
{ fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSans-Regular.ttf' },
{ fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSans-Bold.ttf' },
{ fontStyle: 'italic', fontWeight: 'normal', src: '/fonts/NotoSans-Italic.ttf' },
{ fontStyle: 'italic', fontWeight: 'bold', src: '/fonts/NotoSans-BoldItalic.ttf' },
],
});
// Regex that matches any CJK unified ideographs or CJK punctuation/fullwidth chars
const CJK_RE = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u3000-\u303F\uFF01-\uFF60\uFFE0-\uFFE6]+/g;
Font.register({
family: 'NotoSansMono',
fonts: [
{ fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansMono-Regular.ttf' },
{ fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansMono-Bold.ttf' },
],
});
Font.registerHyphenationCallback((word) => [word]);
hasBaseFonts = true;
};
const registerCJKFonts = (): void => {
if (hasCJKFonts) {
return;
}
Font.register({
family: 'NotoSansSC',
fonts: [
{ fontStyle: 'normal', fontWeight: 'normal', src: '/fonts/NotoSansSC-Regular.otf' },
{ fontStyle: 'normal', fontWeight: 'bold', src: '/fonts/NotoSansSC-Bold.otf' },
],
});
hasCJKFonts = true;
};
const ensureFonts = (content: string): void => {
registerBaseFonts();
if (HAS_CJK_RE.test(content)) {
registerCJKFonts();
}
};
interface TextSegment {
isCJK: boolean;
@@ -610,6 +633,8 @@ function PDFReportDocument({ content }: { content: string }) {
export const generatePDFFromMarkdownNew = async (content: string, fileName: string): Promise<void> => {
try {
ensureFonts(content);
const doc = <PDFReportDocument content={content} />;
const blob = await pdf(doc).toBlob();
@@ -630,6 +655,8 @@ export const generatePDFFromMarkdownNew = async (content: string, fileName: stri
export const generatePDFBlobNew = async (content: string): Promise<Blob> => {
try {
ensureFonts(content);
const doc = <PDFReportDocument content={content} />;
return await pdf(doc).toBlob();