perf(compression): add vite-plugin-compression for gzip and Brotli support (#6279)

Signed-off-by: Balázs Szücs <[email protected]>
This commit is contained in:
brios
2026-06-05 18:49:09 +01:00
committed by GitHub
parent 9866d6e12d
commit 0dff192281
12 changed files with 316 additions and 33 deletions
@@ -110,8 +110,8 @@ jobs:
NPM_CONFIG_IGNORE_SCRIPTS: "true" NPM_CONFIG_IGNORE_SCRIPTS: "true"
working-directory: frontend working-directory: frontend
run: | run: |
mkdir -p src/assets mkdir -p editor/src/assets
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json npx --yes license-report --only=prod --output=json > editor/src/assets/3rdPartyLicenses.json
- name: Postprocess with project script (BASE version) - name: Postprocess with project script (BASE version)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
+2
View File
@@ -57,6 +57,8 @@ app/core/src/main/resources/static/robots.txt
app/core/src/main/resources/static/pdfium/ app/core/src/main/resources/static/pdfium/
app/core/src/main/resources/static/pdfjs/ app/core/src/main/resources/static/pdfjs/
app/core/src/main/resources/static/vendor/ app/core/src/main/resources/static/vendor/
app/core/src/main/resources/static/**/*.gz
app/core/src/main/resources/static/**/*.br
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc. # Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
# Gradle # Gradle
+17 -1
View File
@@ -207,13 +207,29 @@ def resourcesStaticDir = file('src/main/resources/static')
def generatedFrontendPaths = [ def generatedFrontendPaths = [
'assets', 'assets',
'index.html', 'index.html',
'index.html.gz',
'index.html.br',
'sw.js',
'sw.js.gz',
'sw.js.br',
'manifest.json.gz',
'manifest.json.br',
'site.webmanifest.gz',
'site.webmanifest.br',
'browserconfig.xml.gz',
'browserconfig.xml.br',
'manifest-classic.json',
'manifest-classic.json.gz',
'manifest-classic.json.br',
'locales', 'locales',
'Login', 'Login',
'classic-logo', 'classic-logo',
'modern-logo', 'modern-logo',
'og_images', 'og_images',
'samples', 'samples',
'manifest-classic.json' 'pdfium',
'vendor',
'pdfjs'
] ]
tasks.register('npmInstall', Exec) { tasks.register('npmInstall', Exec) {
@@ -13,6 +13,7 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.EncodedResourceResolver;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -49,14 +50,16 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/sw.js", "/manifest.json", "/site.webmanifest", "/browserconfig.xml") "/sw.js", "/manifest.json", "/site.webmanifest", "/browserconfig.xml")
.addResourceLocations(staticPath, "classpath:/static/") .addResourceLocations(staticPath, "classpath:/static/")
.setCacheControl(CacheControl.noStore()) .setCacheControl(CacheControl.noStore())
.resourceChain(true); .resourceChain(true)
.addResolver(new EncodedResourceResolver());
// 2. Vite fingerprinted assets (immutable) // 2. Vite fingerprinted assets (immutable)
// These already have content hashes in filenames (e.g. index-ChAS4tCC.js) // These already have content hashes in filenames (e.g. index-ChAS4tCC.js)
registry.addResourceHandler("/assets/**") registry.addResourceHandler("/assets/**")
.addResourceLocations(staticPath + "assets/", "classpath:/static/assets/") .addResourceLocations(staticPath + "assets/", "classpath:/static/assets/")
.setCacheControl(IMMUTABLE_ONE_YEAR) .setCacheControl(IMMUTABLE_ONE_YEAR)
.resourceChain(true); .resourceChain(true)
.addResolver(new EncodedResourceResolver());
// 3. Media and fonts (immutable) // 3. Media and fonts (immutable)
registry.addResourceHandler("/images/**", "/fonts/**") registry.addResourceHandler("/images/**", "/fonts/**")
@@ -66,7 +69,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
staticPath + "fonts/", staticPath + "fonts/",
"classpath:/static/fonts/") "classpath:/static/fonts/")
.setCacheControl(IMMUTABLE_ONE_YEAR) .setCacheControl(IMMUTABLE_ONE_YEAR)
.resourceChain(true); .resourceChain(true)
.addResolver(new EncodedResourceResolver());
// 4. Branding and stable non-fingerprinted assets (1 day + SWR) // 4. Branding and stable non-fingerprinted assets (1 day + SWR)
// Use stale-while-revalidate to improve perceived performance. // Use stale-while-revalidate to improve perceived performance.
@@ -114,19 +118,27 @@ public class WebMvcConfig implements WebMvcConfigurer {
staticPath + "og_images/", staticPath + "og_images/",
"classpath:/static/og_images/", "classpath:/static/og_images/",
staticPath + "Login/", staticPath + "Login/",
"classpath:/static/Login/") "classpath:/static/Login/",
staticPath + "icons/",
"classpath:/static/icons/",
staticPath + "modern-logo/",
"classpath:/static/modern-logo/",
staticPath + "classic-logo/",
"classpath:/static/classic-logo/")
.setCacheControl( .setCacheControl(
CacheControl.maxAge(Duration.ofDays(1)) CacheControl.maxAge(Duration.ofDays(1))
.cachePublic() .cachePublic()
.staleWhileRevalidate(Duration.ofDays(7))) .staleWhileRevalidate(Duration.ofDays(7)))
.resourceChain(true); .resourceChain(true)
.addResolver(new EncodedResourceResolver());
// 5. Catch-all (SPA fallback) // 5. Catch-all (SPA fallback)
// Must check with server to ensure index.html is always fresh. // Must check with server to ensure index.html is always fresh.
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations(staticPath, "classpath:/static/") .addResourceLocations(staticPath, "classpath:/static/")
.setCacheControl(NO_CACHE) .setCacheControl(NO_CACHE)
.resourceChain(true); .resourceChain(true)
.addResolver(new EncodedResourceResolver());
} }
@Override @Override
@@ -33,7 +33,7 @@ spring.security.filter.dispatcher-types=REQUEST,ERROR
# Response compression # Response compression
server.compression.enabled=true server.compression.enabled=true
server.compression.min-response-size=1024 server.compression.min-response-size=1024
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript,image/svg+xml,application/x-font-ttf,font/opentype,application/vnd.ms-fontobject,font/woff,font/woff2,application/font-woff,application/font-woff2 server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript,image/svg+xml,application/x-font-ttf,font/opentype,application/vnd.ms-fontobject,font/woff,font/woff2,application/font-woff,application/font-woff2,application/wasm
spring.web.error.path=/error spring.web.error.path=/error
spring.web.error.whitelabel.enabled=false spring.web.error.whitelabel.enabled=false
@@ -89,7 +89,7 @@ import { RedactionAPIBridge } from "@app/components/viewer/RedactionAPIBridge";
import { DocumentPermissionsAPIBridge } from "@app/components/viewer/DocumentPermissionsAPIBridge"; import { DocumentPermissionsAPIBridge } from "@app/components/viewer/DocumentPermissionsAPIBridge";
import { DocumentReadyWrapper } from "@app/components/viewer/DocumentReadyWrapper"; import { DocumentReadyWrapper } from "@app/components/viewer/DocumentReadyWrapper";
import { ActiveDocumentProvider } from "@app/components/viewer/ActiveDocumentContext"; import { ActiveDocumentProvider } from "@app/components/viewer/ActiveDocumentContext";
import { absoluteWithBasePath } from "@app/constants/app"; import { pdfiumWasmUrl } from "@app/services/wasmPrecompiler";
import { FormFieldOverlay } from "@app/tools/formFill/FormFieldOverlay"; import { FormFieldOverlay } from "@app/tools/formFill/FormFieldOverlay";
import { ButtonAppearanceOverlay } from "@app/tools/formFill/ButtonAppearanceOverlay"; import { ButtonAppearanceOverlay } from "@app/tools/formFill/ButtonAppearanceOverlay";
import SignatureFieldOverlay from "@app/components/viewer/SignatureFieldOverlay"; import SignatureFieldOverlay from "@app/components/viewer/SignatureFieldOverlay";
@@ -296,7 +296,7 @@ export function LocalEmbedPDF({
// Initialize the engine with the React hook - use local WASM for offline support // Initialize the engine with the React hook - use local WASM for offline support
const { engine, isLoading, error } = usePdfiumEngine({ const { engine, isLoading, error } = usePdfiumEngine({
wasmUrl: absoluteWithBasePath("/pdfium/pdfium.wasm"), wasmUrl: pdfiumWasmUrl,
}); });
// Early return if no file or URL provided // Early return if no file or URL provided
@@ -12,9 +12,34 @@
* `getSignatures`, `saveAsCopy`, …) wrap the `PdfEngine` interface so callers * `getSignatures`, `saveAsCopy`, …) wrap the `PdfEngine` interface so callers
* never have to deal with raw pointers or Tasks. * never have to deal with raw pointers or Tasks.
*/ */
import { init, type WrappedPdfiumModule } from "@embedpdf/pdfium"; import {
init,
type WrappedPdfiumModule,
type PdfiumModule,
} from "@embedpdf/pdfium";
import {
pdfiumWasmModulePromise,
startEagerWasmCompilation,
pdfiumWasmUrl,
} from "@app/services/wasmPrecompiler";
import type { FormField, WidgetCoordinates } from "@app/tools/formFill/types"; import type { FormField, WidgetCoordinates } from "@app/tools/formFill/types";
interface ExtendedPdfiumRuntime {
HEAPU8: Uint8Array;
HEAPF32: Float32Array;
}
interface PdfiumModuleOverrides extends Partial<PdfiumModule> {
locateFile?: (url: string, scriptDirectory?: string) => string;
instantiateWasm?: (
imports: WebAssembly.Imports,
successCallback: (
instance: WebAssembly.Instance,
module: WebAssembly.Module,
) => void,
) => void;
}
// PDF form field type constants (matching PDFium C API FPDF_FORMFIELD_* values) // PDF form field type constants (matching PDFium C API FPDF_FORMFIELD_* values)
const FPDF_FORMFIELD_UNKNOWN = 0; const FPDF_FORMFIELD_UNKNOWN = 0;
const FPDF_FORMFIELD_PUSHBUTTON = 1; const FPDF_FORMFIELD_PUSHBUTTON = 1;
@@ -46,8 +71,7 @@ let _module: WrappedPdfiumModule | null = null;
* Resolve the absolute WASM URL using the same pattern as LocalEmbedPDF. * Resolve the absolute WASM URL using the same pattern as LocalEmbedPDF.
*/ */
function wasmUrl(): string { function wasmUrl(): string {
const base = (import.meta as any).env?.BASE_URL ?? "/"; return pdfiumWasmUrl;
return `${base}pdfium/pdfium.wasm`.replace(/\/\//g, "/");
} }
/** /**
@@ -59,9 +83,47 @@ function wasmUrl(): string {
export async function getPdfiumModule(): Promise<WrappedPdfiumModule> { export async function getPdfiumModule(): Promise<WrappedPdfiumModule> {
if (_module) return _module; if (_module) return _module;
if (!_initPromise) { if (!_initPromise) {
_initPromise = init({ // Ensure eager compilation has started if PDF service is requested before idle timeout
startEagerWasmCompilation();
const overrides: PdfiumModuleOverrides = {
locateFile: () => wasmUrl(), locateFile: () => wasmUrl(),
} as any).then((m) => { };
// Eagerly reuse pre-compiled WASM module from app boot if available
overrides.instantiateWasm = (
imports: WebAssembly.Imports,
successCallback: (
instance: WebAssembly.Instance,
module: WebAssembly.Module,
) => void,
) => {
pdfiumWasmModulePromise
.then((wasmModule) => {
if (wasmModule) {
return WebAssembly.instantiate(wasmModule, imports).then(
(instance) => {
successCallback(instance, wasmModule);
},
);
} else {
throw new Error("No pre-compiled WASM module found");
}
})
.catch((err: unknown) => {
console.warn(
"Eager WebAssembly instantiation failed, falling back to streaming compilation:",
err,
);
WebAssembly.instantiateStreaming(fetch(wasmUrl()), imports).then(
(result) => {
successCallback(result.instance, result.module);
},
);
});
};
_initPromise = init(overrides as Partial<PdfiumModule>).then((m) => {
// Call PDFiumExt_Init to ensure extensions (form fill etc.) are set up // Call PDFiumExt_Init to ensure extensions (form fill etc.) are set up
try { try {
m.PDFiumExt_Init(); m.PDFiumExt_Init();
@@ -110,9 +172,9 @@ export function readAnnotRectAdjusted(
annotPtr: number, annotPtr: number,
rectBuf: number, rectBuf: number,
): boolean { ): boolean {
const ext = (m as any).EPDFAnnot_GetRect; const ext = m.EPDFAnnot_GetRect;
if (typeof ext === "function") { if (typeof ext === "function") {
return ext.call(m, annotPtr, rectBuf); return ext(annotPtr, rectBuf);
} }
return m.FPDFAnnot_GetRect(annotPtr, rectBuf); return m.FPDFAnnot_GetRect(annotPtr, rectBuf);
} }
@@ -186,15 +248,13 @@ export function readEffectivePageBox(
let result: PageBox | null = null; let result: PageBox | null = null;
try { try {
// CropBox is the effective visible area // CropBox is the effective visible area
if ( if (m.FPDFPage_GetCropBox(pagePtr, buf, buf + 4, buf + 8, buf + 12)) {
(m as any).FPDFPage_GetCropBox(pagePtr, buf, buf + 4, buf + 8, buf + 12)
) {
result = read(); result = read();
} }
// Fall back to MediaBox // Fall back to MediaBox
if ( if (
!result && !result &&
(m as any).FPDFPage_GetMediaBox(pagePtr, buf, buf + 4, buf + 8, buf + 12) m.FPDFPage_GetMediaBox(pagePtr, buf, buf + 4, buf + 8, buf + 12)
) { ) {
result = read(); result = read();
} }
@@ -226,7 +286,7 @@ function copyToWasmHeap(
bytes: Uint8Array, bytes: Uint8Array,
ptr: number, ptr: number,
): void { ): void {
new Uint8Array((m.pdfium.wasmExports as any).memory.buffer).set(bytes, ptr); (m.pdfium as typeof m.pdfium & ExtendedPdfiumRuntime).HEAPU8.set(bytes, ptr);
} }
/** /**
@@ -1338,9 +1398,13 @@ async function renderWidgetAppearance(
formEnvPtr: number, formEnvPtr: number,
dpr: number, dpr: number,
): Promise<ImageData | null> { ): Promise<ImageData | null> {
const pdfiumWasm = m.pdfium as any;
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4); const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
const matrixView = new Float32Array(pdfiumWasm.HEAPF32.buffer, matrixPtr, 6); const pdfiumRuntime = m.pdfium as typeof m.pdfium & ExtendedPdfiumRuntime;
const matrixView = new Float32Array(
pdfiumRuntime.HEAPF32.buffer,
matrixPtr,
6,
);
const sx = wDev / pdfW; const sx = wDev / pdfW;
const sy = hDev / pdfH; const sy = hDev / pdfH;
matrixView.set([sx, 0, 0, -sy, -sx * annotLeft, sy * annotTop]); matrixView.set([sx, 0, 0, -sy, -sx * annotLeft, sy * annotTop]);
@@ -1364,7 +1428,7 @@ async function renderWidgetAppearance(
let imageData: ImageData | null = null; let imageData: ImageData | null = null;
if (ok) { if (ok) {
const rgba = new Uint8ClampedArray( const rgba = new Uint8ClampedArray(
pdfiumWasm.HEAPU8.buffer.slice(heapPtr, heapPtr + bytes), pdfiumRuntime.HEAPU8.subarray(heapPtr, heapPtr + bytes),
); );
let hasVisible = false; let hasVisible = false;
for (let i = 3; i < rgba.length; i += 4) { for (let i = 3; i < rgba.length; i += 4) {
@@ -1417,7 +1481,7 @@ async function renderWidgetAppearance(
m.FPDFBitmap_Destroy(bmp2); m.FPDFBitmap_Destroy(bmp2);
const rgba2 = new Uint8ClampedArray( const rgba2 = new Uint8ClampedArray(
pdfiumWasm.HEAPU8.buffer.slice(heap2, heap2 + bytes), pdfiumRuntime.HEAPU8.subarray(heap2, heap2 + bytes),
); );
let hasVisible2 = false; let hasVisible2 = false;
for (let i = 3; i < rgba2.length; i += 4) { for (let i = 3; i < rgba2.length; i += 4) {
@@ -1560,8 +1624,6 @@ export async function renderSignatureFieldAppearances(
const hDev = Math.max(1, Math.round(pdfH * dpr)); const hDev = Math.max(1, Math.round(pdfH * dpr));
const stride = wDev * 4; const stride = wDev * 4;
const bytes = stride * hDev; const bytes = stride * hDev;
const pdfiumWasm = m.pdfium as any;
const heapPtr = m.pdfium.wasmExports.malloc(bytes); const heapPtr = m.pdfium.wasmExports.malloc(bytes);
const bitmapPtr = m.FPDFBitmap_CreateEx( const bitmapPtr = m.FPDFBitmap_CreateEx(
wDev, wDev,
@@ -1577,8 +1639,10 @@ export async function renderSignatureFieldAppearances(
const sx = wDev / pdfW; const sx = wDev / pdfW;
const sy = hDev / pdfH; const sy = hDev / pdfH;
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4); const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
const pdfiumRuntime = m.pdfium as typeof m.pdfium &
ExtendedPdfiumRuntime;
const matrixView = new Float32Array( const matrixView = new Float32Array(
pdfiumWasm.HEAPF32.buffer, pdfiumRuntime.HEAPF32.buffer,
matrixPtr, matrixPtr,
6, 6,
); );
@@ -1603,7 +1667,7 @@ export async function renderSignatureFieldAppearances(
if (ok) { if (ok) {
const rgba = new Uint8ClampedArray( const rgba = new Uint8ClampedArray(
pdfiumWasm.HEAPU8.buffer.slice(heapPtr, heapPtr + bytes), pdfiumRuntime.HEAPU8.subarray(heapPtr, heapPtr + bytes),
); );
let hasVisible = false; let hasVisible = false;
for (let i = 3; i < rgba.length; i += 4) { for (let i = 3; i < rgba.length; i += 4) {
@@ -1663,7 +1727,7 @@ export async function renderSignatureFieldAppearances(
m.FPDFBitmap_Destroy(bmp2); m.FPDFBitmap_Destroy(bmp2);
const rgba2 = new Uint8ClampedArray( const rgba2 = new Uint8ClampedArray(
pdfiumWasm.HEAPU8.buffer.slice(heap2, heap2 + bytes), pdfiumRuntime.HEAPU8.subarray(heap2, heap2 + bytes),
); );
let hasVisible2 = false; let hasVisible2 = false;
for (let i = 3; i < rgba2.length; i += 4) { for (let i = 3; i < rgba2.length; i += 4) {
@@ -0,0 +1,54 @@
import { BASE_PATH } from "@app/constants/app";
import pdfiumWasmAssetUrl from "@embedpdf/pdfium/pdfium.wasm?url";
const getWasmUrl = (): string => {
if (
pdfiumWasmAssetUrl.startsWith("http://") ||
pdfiumWasmAssetUrl.startsWith("https://") ||
pdfiumWasmAssetUrl.startsWith("//")
) {
return pdfiumWasmAssetUrl;
}
const origin = typeof window !== "undefined" ? window.location.origin : "";
if (import.meta.env.DEV) {
return `${origin}${BASE_PATH}/pdfium/pdfium.wasm`;
}
const cleanAssetUrl = pdfiumWasmAssetUrl
.replace(/^\.\//, "")
.replace(/^\//, "");
return `${origin}${BASE_PATH}/${cleanAssetUrl}`;
};
export const pdfiumWasmUrl = getWasmUrl();
let resolvePromise: (module: WebAssembly.Module | null) => void;
let compilationStarted = false;
export const pdfiumWasmModulePromise = new Promise<WebAssembly.Module | null>(
(resolve) => {
resolvePromise = resolve;
},
);
export function startEagerWasmCompilation(): void {
if (compilationStarted) return;
compilationStarted = true;
if (
typeof WebAssembly === "object" &&
typeof WebAssembly.compileStreaming === "function"
) {
WebAssembly.compileStreaming(fetch(pdfiumWasmUrl))
.then(resolvePromise)
.catch((err) => {
console.warn(
"Eager WASM compilation failed or not supported in this environment:",
err,
);
resolvePromise(null);
});
} else {
resolvePromise(null);
}
}
+18
View File
@@ -17,6 +17,24 @@ import posthog from "posthog-js";
import { PostHogProvider } from "@posthog/react"; import { PostHogProvider } from "@posthog/react";
import { BASE_PATH } from "@app/constants/app"; import { BASE_PATH } from "@app/constants/app";
import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler";
if (typeof window !== "undefined") {
const scheduleCompilation = () => {
if (typeof requestIdleCallback === "function") {
requestIdleCallback(() => startEagerWasmCompilation(), { timeout: 2000 });
} else {
setTimeout(startEagerWasmCompilation, 1000);
}
};
if (document.readyState === "complete") {
scheduleCompilation();
} else {
window.addEventListener("load", scheduleCompilation);
}
}
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, { posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: "2025-05-24", defaults: "2025-05-24",
+97
View File
@@ -2,6 +2,76 @@ import { defineConfig, loadEnv, type PluginOption } from "vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { viteStaticCopy } from "vite-plugin-static-copy"; import { viteStaticCopy } from "vite-plugin-static-copy";
import { compression, defineAlgorithm } from "vite-plugin-compression2";
import { constants, gzip, brotliCompress } from "node:zlib";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import fs from "node:fs/promises";
import path from "node:path";
const gzipPromise = promisify(gzip);
const brotliPromise = promisify(brotliCompress);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function compressStaticCopyPlugin(): PluginOption {
return {
name: "compress-static-copy",
apply: "build" as const,
async closeBundle() {
const distDir = path.resolve(__dirname, "dist");
const targets = ["pdfium", "vendor", "pdfjs"];
const excludedExtensions = [
".gz",
".br",
".png",
".jpg",
".jpeg",
".gif",
".webp",
".woff",
".woff2",
];
async function walkAndCompress(dirOrFile: string) {
let stat;
try {
stat = await fs.stat(dirOrFile);
} catch {
return;
}
if (stat.isFile()) {
const ext = path.extname(dirOrFile).toLowerCase();
if (stat.size >= 1024 && !excludedExtensions.includes(ext)) {
const content = await fs.readFile(dirOrFile);
// Gzip (level 9)
const gzipped = await gzipPromise(content, { level: 9 });
await fs.writeFile(`${dirOrFile}.gz`, gzipped);
// Brotli (quality 11)
const brotlied = await brotliPromise(content, {
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
},
});
await fs.writeFile(`${dirOrFile}.br`, brotlied);
}
} else if (stat.isDirectory()) {
const files = await fs.readdir(dirOrFile);
for (const file of files) {
await walkAndCompress(path.join(dirOrFile, file));
}
}
}
for (const target of targets) {
await walkAndCompress(path.join(distDir, target));
}
},
};
}
const VALID_MODES = [ const VALID_MODES = [
"core", "core",
@@ -78,6 +148,18 @@ export default defineConfig(async ({ mode }) => {
tsconfigPaths({ tsconfigPaths({
projects: [tsconfigProject], projects: [tsconfigProject],
}), }),
compression({
threshold: 1024,
exclude: [/\.(png|jpg|jpeg|gif|webp|woff|woff2)$/],
algorithms: [
defineAlgorithm("gzip", { level: 9 }),
defineAlgorithm("brotliCompress", {
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
},
}),
],
}),
// Set ANALYZE=true to emit dist/stats.html (treemap) alongside the // Set ANALYZE=true to emit dist/stats.html (treemap) alongside the
// build; rollup-plugin-visualizer is ESM-only so we import dynamically. // build; rollup-plugin-visualizer is ESM-only so we import dynamically.
...(process.env.ANALYZE === "true" ...(process.env.ANALYZE === "true"
@@ -119,6 +201,7 @@ export default defineConfig(async ({ mode }) => {
}, },
], ],
}), }),
compressStaticCopyPlugin(),
], ],
server: { server: {
host: true, host: true,
@@ -139,6 +222,20 @@ export default defineConfig(async ({ mode }) => {
strictPort: true, strictPort: true,
proxy: backendProxyConfig, proxy: backendProxyConfig,
}, },
build: {
target: "esnext",
rollupOptions: {
output: {
manualChunks: {
"vendor-react": ["react", "react-dom"],
"pdf-engine": ["@embedpdf/engines", "@embedpdf/pdfium"],
},
},
},
},
optimizeDeps: {
exclude: ["@embedpdf/pdfium"],
},
// base: "./" produces relative asset URLs which work when dist/ is served // base: "./" produces relative asset URLs which work when dist/ is served
// at any path (e.g. Spring Boot bundling the frontend at /). But under // at any path (e.g. Spring Boot bundling the frontend at /). But under
// `vite preview` for deep SPA routes (e.g. /workflow/sign/<token>), the // `vite preview` for deep SPA routes (e.g. /workflow/sign/<token>), the
+19
View File
@@ -132,6 +132,7 @@
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.44.1", "typescript-eslint": "^8.44.1",
"vite": "^7.3.2", "vite": "^7.3.2",
"vite-plugin-compression2": "^2.5.3",
"vite-plugin-static-copy": "^3.1.4", "vite-plugin-static-copy": "^3.1.4",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"
@@ -16503,6 +16504,13 @@
"bare-path": "^3.0.0" "bare-path": "^3.0.0"
} }
}, },
"node_modules/tar-mini": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
"integrity": "sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ==",
"dev": true,
"license": "MIT"
},
"node_modules/tar-stream": { "node_modules/tar-stream": {
"version": "3.1.8", "version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
@@ -17412,6 +17420,17 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/vite-plugin-compression2": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-2.5.3.tgz",
"integrity": "sha512-ItPgqQWkcnBbVw7is9OKwiZ8v6+ju9rYROl5Lp6QfQDEx/d55AwJQb/KLpsQqsU9HoigYBsZ8tK6I02UwJNvEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"tar-mini": "^0.2.0"
}
},
"node_modules/vite-plugin-static-copy": { "node_modules/vite-plugin-static-copy": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz",
+1
View File
@@ -152,6 +152,7 @@
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.44.1", "typescript-eslint": "^8.44.1",
"vite": "^7.3.2", "vite": "^7.3.2",
"vite-plugin-compression2": "^2.5.3",
"vite-plugin-static-copy": "^3.1.4", "vite-plugin-static-copy": "^3.1.4",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"