server: (webui) no more gzip compression (#21073)

* webui: no more gzip

* try changing a small line

* Revert "try changing a small line"

This reverts commit 0d7a353159.

* fix lint

* fix test

* rebuild

* split into html/css/js

* lint

* chore: update webui build output

* chore: Update git hooks script

* server: update webui build output

* chore: Update pre-commit hook

* refactor: Cleanup

---------

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>
This commit is contained in:
Xuan-Son Nguyen 2026-03-31 15:44:26 +02:00 committed by GitHub
parent 624733d631
commit 4a00bbfed6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 608 additions and 209 deletions

View File

@ -21,14 +21,6 @@ indent_style = tab
[prompts/*.txt]
insert_final_newline = unset
[tools/server/public/*]
indent_size = 2
[tools/server/public/deps_*]
trim_trailing_whitespace = unset
indent_style = unset
indent_size = unset
[tools/server/deps_*]
trim_trailing_whitespace = unset
indent_style = unset
@ -61,6 +53,14 @@ charset = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
[tools/server/public/**]
indent_style = unset
indent_size = unset
end_of_line = unset
charset = unset
trim_trailing_whitespace = unset
insert_final_newline = unset
[benches/**]
indent_style = unset
indent_size = unset

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
# Treat the generated single-file WebUI build as binary for diff purposes.
# Git's pack-file delta compression still works (byte-level), but this prevents
# git diff from printing the entire minified file on every change.
tools/server/public/index.html -diff

2
.gitignore vendored
View File

@ -95,6 +95,8 @@
# Server Web UI temporary files
/tools/server/webui/node_modules
/tools/server/webui/dist
# we no longer use gz for index.html
/tools/server/public/index.html.gz
# Python

View File

@ -42,7 +42,9 @@ option(LLAMA_BUILD_WEBUI "Build the embedded Web UI" ON)
if (LLAMA_BUILD_WEBUI)
set(PUBLIC_ASSETS
index.html.gz
index.html
bundle.js
bundle.css
loading.html
)

View File

@ -259,6 +259,6 @@ npm run test
npm run build
```
After `public/index.html.gz` has been generated, rebuild `llama-server` as described in the [build](#build) section to include the updated UI.
After `public/index.html` has been generated, rebuild `llama-server` as described in the [build](#build) section to include the updated UI.
**Note:** The Vite dev server automatically proxies API requests to `http://localhost:8080`. Make sure `llama-server` is running on that port during development.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -10,7 +10,9 @@
#ifdef LLAMA_BUILD_WEBUI
// auto generated files (see README.md for details)
#include "index.html.gz.hpp"
#include "index.html.hpp"
#include "bundle.js.hpp"
#include "bundle.css.hpp"
#include "loading.html.hpp"
#endif
@ -272,16 +274,19 @@ bool server_http_context::init(const common_params & params) {
} else {
#ifdef LLAMA_BUILD_WEBUI
// using embedded static index.html
srv->Get(params.api_prefix + "/", [](const httplib::Request & req, httplib::Response & res) {
if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {
res.set_content("Error: gzip is not supported by this browser", "text/plain");
} else {
res.set_header("Content-Encoding", "gzip");
// COEP and COOP headers, required by pyodide (python interpreter)
res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
res.set_header("Cross-Origin-Opener-Policy", "same-origin");
res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");
}
srv->Get(params.api_prefix + "/", [](const httplib::Request & /*req*/, httplib::Response & res) {
// COEP and COOP headers, required by pyodide (python interpreter)
res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
res.set_header("Cross-Origin-Opener-Policy", "same-origin");
res.set_content(reinterpret_cast<const char*>(index_html), index_html_len, "text/html; charset=utf-8");
return false;
});
srv->Get(params.api_prefix + "/bundle.js", [](const httplib::Request & /*req*/, httplib::Response & res) {
res.set_content(reinterpret_cast<const char*>(bundle_js), bundle_js_len, "application/javascript; charset=utf-8");
return false;
});
srv->Get(params.api_prefix + "/bundle.css", [](const httplib::Request & /*req*/, httplib::Response & res) {
res.set_content(reinterpret_cast<const char*>(bundle_css), bundle_css_len, "text/css; charset=utf-8");
return false;
});
#endif

View File

@ -188,14 +188,14 @@ The build process:
1. **Vite Build** - Bundles all TypeScript, Svelte, and CSS
2. **Static Adapter** - Outputs to `../public` (llama-server's static file directory)
3. **Post-Build Script** - Cleans up intermediate files
4. **Custom Plugin** - Creates `index.html.gz` with:
4. **Custom Plugin** - Creates `index.html` with:
- Inlined favicon as base64
- GZIP compression (level 9)
- Deterministic output (zeroed timestamps)
```text
tools/server/webui/ → build → tools/server/public/
├── src/ ├── index.html.gz (served by llama-server)
├── src/ ├── index.html (served by llama-server)
├── static/ └── (favicon inlined)
└── ...
```
@ -219,7 +219,7 @@ output: {
The WebUI is embedded directly into the llama-server binary:
1. `npm run build` outputs `index.html.gz` to `tools/server/public/`
1. `npm run build` outputs `index.html` to `tools/server/public/`
2. llama-server compiles this into the binary at build time
3. When accessing `/`, llama-server serves the gzipped HTML
4. All assets are inlined (CSS, JS, fonts, favicon)

View File

@ -50,7 +50,6 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-storybook": "^10.2.4",
"eslint-plugin-svelte": "^3.0.0",
"fflate": "^0.8.2",
"globals": "^16.0.0",
"http-server": "^14.1.1",
"mdast": "^3.0.0",

View File

@ -1,14 +1,12 @@
#!/bin/bash
# Script to install pre-commit and pre-push hooks for webui
# Pre-commit: formats code and runs checks
# Pre-push: builds the project, stashes unstaged changes
# Script to install pre-commit hook for webui
# Pre-commit: formats, checks, builds, and stages build output
REPO_ROOT=$(git rev-parse --show-toplevel)
PRE_COMMIT_HOOK="$REPO_ROOT/.git/hooks/pre-commit"
PRE_PUSH_HOOK="$REPO_ROOT/.git/hooks/pre-push"
echo "Installing pre-commit and pre-push hooks for webui..."
echo "Installing pre-commit hook for webui..."
# Create the pre-commit hook
cat > "$PRE_COMMIT_HOOK" << 'EOF'
@ -16,21 +14,19 @@ cat > "$PRE_COMMIT_HOOK" << 'EOF'
# Check if there are any changes in the webui directory
if git diff --cached --name-only | grep -q "^tools/server/webui/"; then
echo "Formatting and checking webui code..."
# Change to webui directory and run format
cd tools/server/webui
# Check if npm is available and package.json exists
REPO_ROOT=$(git rev-parse --show-toplevel)
cd "$REPO_ROOT/tools/server/webui"
# Check if package.json exists
if [ ! -f "package.json" ]; then
echo "Error: package.json not found in tools/server/webui"
exit 1
fi
echo "Formatting and checking webui code..."
# Run the format command
npm run format
# Check if format command succeeded
if [ $? -ne 0 ]; then
echo "Error: npm run format failed"
exit 1
@ -38,8 +34,6 @@ if git diff --cached --name-only | grep -q "^tools/server/webui/"; then
# Run the lint command
npm run lint
# Check if lint command succeeded
if [ $? -ne 0 ]; then
echo "Error: npm run lint failed"
exit 1
@ -47,156 +41,42 @@ if git diff --cached --name-only | grep -q "^tools/server/webui/"; then
# Run the check command
npm run check
# Check if check command succeeded
if [ $? -ne 0 ]; then
echo "Error: npm run check failed"
exit 1
fi
# Go back to repo root
cd ../../..
echo "✅ Webui code formatted and checked successfully"
fi
exit 0
EOF
# Create the pre-push hook
cat > "$PRE_PUSH_HOOK" << 'EOF'
#!/bin/bash
# Check if there are any webui changes that need building
WEBUI_CHANGES=$(git diff --name-only @{push}..HEAD | grep "^tools/server/webui/" || true)
if [ -n "$WEBUI_CHANGES" ]; then
echo "Webui changes detected, checking if build is up-to-date..."
# Change to webui directory
cd tools/server/webui
# Check if npm is available and package.json exists
if [ ! -f "package.json" ]; then
echo "Error: package.json not found in tools/server/webui"
# Build the webui
echo "Building webui..."
npm run build
if [ $? -ne 0 ]; then
echo "❌ npm run build failed"
exit 1
fi
# Check if build output exists and is newer than source files
BUILD_FILE="../public/index.html.gz"
NEEDS_BUILD=false
if [ ! -f "$BUILD_FILE" ]; then
echo "Build output not found, building..."
NEEDS_BUILD=true
else
# Check if any source files are newer than the build output
if find src -newer "$BUILD_FILE" -type f | head -1 | grep -q .; then
echo "Source files are newer than build output, rebuilding..."
NEEDS_BUILD=true
fi
fi
if [ "$NEEDS_BUILD" = true ]; then
echo "Building webui..."
# Stash any unstaged changes to avoid conflicts during build
echo "Checking for unstaged changes..."
if ! git diff --quiet || ! git diff --cached --quiet --diff-filter=A; then
echo "Stashing unstaged changes..."
git stash push --include-untracked -m "Pre-push hook: stashed unstaged changes"
STASH_CREATED=$?
else
echo "No unstaged changes to stash"
STASH_CREATED=1
fi
# Run the build command
npm run build
# Check if build command succeeded
if [ $? -ne 0 ]; then
echo "Error: npm run build failed"
if [ $STASH_CREATED -eq 0 ]; then
echo "You can restore your unstaged changes with: git stash pop"
fi
exit 1
fi
# Go back to repo root
cd ../../..
# Check if build output was created/updated
if [ -f "tools/server/public/index.html.gz" ]; then
# Add the build output and commit it
git add tools/server/public/index.html.gz
if ! git diff --cached --quiet; then
echo "Committing updated build output..."
git commit -m "chore: update webui build output"
echo "✅ Build output committed successfully"
else
echo "Build output unchanged"
fi
else
echo "Error: Build output not found after build"
if [ $STASH_CREATED -eq 0 ]; then
echo "You can restore your unstaged changes with: git stash pop"
fi
exit 1
fi
if [ $STASH_CREATED -eq 0 ]; then
echo "✅ Build completed. Your unstaged changes have been stashed."
echo "They will be automatically restored after the push."
# Create a marker file to indicate stash was created by pre-push hook
touch .git/WEBUI_PUSH_STASH_MARKER
fi
else
echo "✅ Build output is up-to-date"
fi
echo "✅ Webui ready for push"
# Stage the build output alongside the source changes
cd "$REPO_ROOT"
git add tools/server/public/
echo "✅ Webui built and build output staged"
fi
exit 0
EOF
# Create the post-push hook (for restoring stashed changes after push)
cat > "$REPO_ROOT/.git/hooks/post-push" << 'EOF'
#!/bin/bash
# Check if we have a stash marker from the pre-push hook
if [ -f .git/WEBUI_PUSH_STASH_MARKER ]; then
echo "Restoring your unstaged changes after push..."
git stash pop
rm -f .git/WEBUI_PUSH_STASH_MARKER
echo "✅ Your unstaged changes have been restored."
fi
exit 0
EOF
# Make all hooks executable
# Make hook executable
chmod +x "$PRE_COMMIT_HOOK"
chmod +x "$PRE_PUSH_HOOK"
chmod +x "$REPO_ROOT/.git/hooks/post-push"
if [ $? -eq 0 ]; then
echo "✅ Git hooks installed successfully!"
echo "✅ Git hook installed successfully!"
echo " Pre-commit: $PRE_COMMIT_HOOK"
echo " Pre-push: $PRE_PUSH_HOOK"
echo " Post-push: $REPO_ROOT/.git/hooks/post-push"
echo ""
echo "The hooks will automatically:"
echo " • Format and check webui code before commits (pre-commit)"
echo " • Build webui code before pushes (pre-push)"
echo " • Stash unstaged changes during build process"
echo " • Restore your unstaged changes after the push"
echo ""
echo "To test the hooks:"
echo " • Make a change to a file in the webui directory and commit it (triggers format/check)"
echo " • Push your commits to trigger the build process"
echo "The hook will automatically:"
echo " • Format, lint and check webui code before commits"
echo " • Build webui and stage tools/server/public/ into the same commit"
else
echo "❌ Failed to make hooks executable"
echo "❌ Failed to make hook executable"
exit 1
fi

View File

@ -1,3 +1,3 @@
rm -rf ../public/_app;
rm ../public/favicon.svg;
rm ../public/index.html;
rm -f ../public/index.html.gz; # deprecated, but may still be generated by older versions of the build process

View File

@ -40,6 +40,17 @@
--code-background: oklch(0.985 0 0);
--code-foreground: oklch(0.145 0 0);
--layer-popover: 1000000;
--chat-form-area-height: 8rem;
--chat-form-area-offset: 2rem;
--max-message-height: max(24rem, min(80dvh, calc(100dvh - var(--chat-form-area-height) - 12rem)));
}
@media (min-width: 640px) {
:root {
--chat-form-area-height: 24rem;
--chat-form-area-offset: 12rem;
}
}
.dark {
@ -116,19 +127,6 @@
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--chat-form-area-height: 8rem;
--chat-form-area-offset: 2rem;
--max-message-height: max(24rem, min(80dvh, calc(100dvh - var(--chat-form-area-height) - 12rem)));
}
@media (min-width: 640px) {
:root {
--chat-form-area-height: 24rem;
--chat-form-area-offset: 12rem;
}
}
@layer base {
* {
@apply border-border outline-ring/50;

View File

@ -21,7 +21,7 @@ const config = {
strict: true
}),
output: {
bundleStrategy: 'inline'
bundleStrategy: 'single'
},
alias: {
$styles: 'src/styles'

View File

@ -2,5 +2,5 @@ import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('h1').first()).toBeVisible();
});

View File

@ -1,7 +1,6 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import * as fflate from 'fflate';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { readFileSync, writeFileSync, existsSync, readdirSync, copyFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
@ -20,15 +19,13 @@ const GUIDE_FOR_FRONTEND = `
-->
`.trim();
const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
/**
* the maximum size of an embedded asset in bytes,
* e.g. maximum size of embedded font (see node_modules/katex/dist/fonts/*.woff2)
*/
const MAX_ASSET_SIZE = 32000;
/** public/index.html.gz minified flag */
/** public/index.html minified flag */
const ENABLE_JS_MINIFICATION = true;
function llamaCppBuildPlugin() {
@ -40,7 +37,6 @@ function llamaCppBuildPlugin() {
setTimeout(() => {
try {
const indexPath = resolve('../public/index.html');
const gzipPath = resolve('../public/index.html.gz');
if (!existsSync(indexPath)) {
return;
@ -61,26 +57,35 @@ function llamaCppBuildPlugin() {
content = content.replace(/\r/g, '');
content = GUIDE_FOR_FRONTEND + '\n' + content;
content = content.replace(/\/_app\/immutable\/bundle\.[^"]+\.js/g, './bundle.js');
content = content.replace(
/\/_app\/immutable\/assets\/bundle\.[^"]+\.css/g,
'./bundle.css'
);
const compressed = fflate.gzipSync(Buffer.from(content, 'utf-8'), { level: 9 });
writeFileSync(indexPath, content, 'utf-8');
console.log('✓ Updated index.html');
compressed[0x4] = 0;
compressed[0x5] = 0;
compressed[0x6] = 0;
compressed[0x7] = 0;
compressed[0x9] = 0;
if (compressed.byteLength > MAX_BUNDLE_SIZE) {
throw new Error(
`Bundle size is too large (${Math.ceil(compressed.byteLength / 1024)} KB).\n` +
`Please reduce the size of the frontend or increase MAX_BUNDLE_SIZE in vite.config.ts.\n`
);
// Copy bundle.*.js -> ../public/bundle.js
const immutableDir = resolve('../public/_app/immutable');
const bundleDir = resolve('../public/_app/immutable/assets');
if (existsSync(immutableDir)) {
const jsFiles = readdirSync(immutableDir).filter((f) => f.match(/^bundle\..+\.js$/));
if (jsFiles.length > 0) {
copyFileSync(resolve(immutableDir, jsFiles[0]), resolve('../public/bundle.js'));
console.log(`✓ Copied ${jsFiles[0]} -> bundle.js`);
}
}
// Copy bundle.*.css -> ../public/bundle.css
if (existsSync(bundleDir)) {
const cssFiles = readdirSync(bundleDir).filter((f) => f.match(/^bundle\..+\.css$/));
if (cssFiles.length > 0) {
copyFileSync(resolve(bundleDir, cssFiles[0]), resolve('../public/bundle.css'));
console.log(`✓ Copied ${cssFiles[0]} -> bundle.css`);
}
}
writeFileSync(gzipPath, compressed);
console.log('✓ Created index.html.gz');
} catch (error) {
console.error('Failed to create gzip file:', error);
console.error('Failed to update index.html:', error);
}
}, 100);
}