From 755452641079e6bba2e94c6b0f73fe92c0d8161f Mon Sep 17 00:00:00 2001 From: Sumeet213 Date: Mon, 1 Dec 2025 16:30:03 +0530 Subject: [PATCH] feat: add pre-built compression and code splitting for frontend assets --- backend/chainlit/server.py | 38 ++++++++++++++++++++++++++-- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 42 ++++++++++++++++++++++++++++++ frontend/vite.config.ts | 52 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 8de929f244..e2066fcb81 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -275,12 +275,38 @@ async def serve_public_file( raise HTTPException(status_code=404, detail="File not found") +def get_precompressed_response( + file_path: Path, accept_encoding: str +) -> Optional[FileResponse]: + content_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream" + + if "br" in accept_encoding: + br_path = file_path.with_suffix(file_path.suffix + ".br") + if br_path.is_file(): + return FileResponse( + br_path, + media_type=content_type, + headers={"Content-Encoding": "br", "Vary": "Accept-Encoding"}, + ) + + if "gzip" in accept_encoding: + gz_path = file_path.with_suffix(file_path.suffix + ".gz") + if gz_path.is_file(): + return FileResponse( + gz_path, + media_type=content_type, + headers={"Content-Encoding": "gzip", "Vary": "Accept-Encoding"}, + ) + + return None + + @router.get("/assets/{filename:path}") async def serve_asset_file( + request: Request, filename: str, ): """Serve a file from assets dir.""" - base_path = Path(os.path.join(build_dir, "assets")) file_path = (base_path / filename).resolve() @@ -288,6 +314,10 @@ async def serve_asset_file( raise HTTPException(status_code=400, detail="Invalid filename") if file_path.is_file(): + accept_encoding = request.headers.get("accept-encoding", "") + precompressed = get_precompressed_response(file_path, accept_encoding) + if precompressed: + return precompressed return FileResponse(file_path) else: raise HTTPException(status_code=404, detail="File not found") @@ -295,10 +325,10 @@ async def serve_asset_file( @router.get("/copilot/{filename:path}") async def serve_copilot_file( + request: Request, filename: str, ): """Serve a file from assets dir.""" - base_path = Path(copilot_build_dir) file_path = (base_path / filename).resolve() @@ -306,6 +336,10 @@ async def serve_copilot_file( raise HTTPException(status_code=400, detail="Invalid filename") if file_path.is_file(): + accept_encoding = request.headers.get("accept-encoding", "") + precompressed = get_precompressed_response(file_path, accept_encoding) + if precompressed: + return precompressed return FileResponse(file_path) else: raise HTTPException(status_code=404, detail="File not found") diff --git a/frontend/package.json b/frontend/package.json index 67f24e62fb..90bf067224 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -96,6 +96,7 @@ "tslib": "^2.6.2", "typescript": "^5.2.2", "vite": "^5.4.14", + "vite-plugin-compression": "^0.5.1", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.2.0", "vitest": "^0.34.4" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 122189b8d5..4aca95581b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -268,6 +268,9 @@ importers: vite: specifier: ^5.4.14 version: 5.4.14(@types/node@20.5.7) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@5.4.14(@types/node@20.5.7)) vite-plugin-svgr: specifier: ^4.2.0 version: 4.2.0(rollup@4.31.0)(typescript@5.2.2)(vite@5.4.14(@types/node@20.5.7)) @@ -2727,6 +2730,10 @@ packages: from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3198,6 +3205,9 @@ packages: jsonc-parser@3.2.1: resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + katex@0.16.22: resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} hasBin: true @@ -4348,6 +4358,10 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unquote@1.1.1: resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} @@ -4422,6 +4436,11 @@ packages: engines: {node: '>=v14.18.0'} hasBin: true + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=4.5.4' + vite-plugin-svgr@4.2.0: resolution: {integrity: sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==} peerDependencies: @@ -7058,6 +7077,12 @@ snapshots: inherits: 2.0.4 readable-stream: 2.3.8 + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true @@ -7641,6 +7666,12 @@ snapshots: jsonc-parser@3.2.1: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + katex@0.16.22: dependencies: commander: 8.3.0 @@ -9225,6 +9256,8 @@ snapshots: universalify@0.2.0: {} + universalify@2.0.1: {} + unquote@1.1.1: {} update-browserslist-db@1.0.13(browserslist@4.22.2): @@ -9309,6 +9342,15 @@ snapshots: - supports-color - terser + vite-plugin-compression@0.5.1(vite@5.4.14(@types/node@20.5.7)): + dependencies: + chalk: 4.1.2 + debug: 4.3.4 + fs-extra: 10.1.0 + vite: 5.4.14(@types/node@20.5.7) + transitivePeerDependencies: + - supports-color + vite-plugin-svgr@4.2.0(rollup@4.31.0)(typescript@5.2.2)(vite@5.4.14(@types/node@20.5.7)): dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.31.0) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 57733d1871..29dc049e16 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,15 +1,63 @@ import react from '@vitejs/plugin-react-swc'; import path from 'path'; import { defineConfig } from 'vite'; +import viteCompression from 'vite-plugin-compression'; import svgr from 'vite-plugin-svgr'; import tsconfigPaths from 'vite-tsconfig-paths'; // https://vitejs.dev/config/ export default defineConfig({ build: { - sourcemap: true + sourcemap: true, + rollupOptions: { + output: { + manualChunks: { + 'vendor-react': ['react', 'react-dom', 'react-router-dom'], + 'vendor-ui': [ + '@radix-ui/react-dialog', + '@radix-ui/react-dropdown-menu', + '@radix-ui/react-popover', + '@radix-ui/react-select', + '@radix-ui/react-tabs', + '@radix-ui/react-tooltip', + '@radix-ui/react-accordion', + '@radix-ui/react-checkbox', + '@radix-ui/react-switch', + '@radix-ui/react-slider', + '@radix-ui/react-scroll-area', + 'lucide-react' + ], + 'vendor-markdown': [ + 'react-markdown', + 'rehype-katex', + 'rehype-raw', + 'remark-gfm', + 'remark-math', + 'remark-directive', + 'highlight.js' + ], + 'vendor-utils': ['lodash', 'uuid', 'zod', 'i18next', 'react-i18next'] + } + } + } }, - plugins: [react(), tsconfigPaths(), svgr()], + plugins: [ + react(), + tsconfigPaths(), + svgr(), + viteCompression({ + algorithm: 'gzip', + ext: '.gz', + threshold: 1024, + deleteOriginFile: false + }), + viteCompression({ + algorithm: 'brotliCompress', + ext: '.br', + threshold: 1024, + deleteOriginFile: false + }) + ], resolve: { alias: { '@': path.resolve(__dirname, './src'),