Stop WebUI startup scripts from blocking render

Load startup vendor scripts with defer and serve Bootstrap locally so the first WebUI mount no longer depends on a parser-blocking CDN request.

Add a focused regression that rejects undeferred classic startup scripts in index.html and documents the vendor/startup contract.
This commit is contained in:
Alessandro 2026-07-08 01:28:36 +02:00
parent 7aba42d46e
commit 5ac4f75a47
6 changed files with 80 additions and 8 deletions

View file

@ -0,0 +1,40 @@
from pathlib import Path
from html.parser import HTMLParser
PROJECT_ROOT = Path(__file__).resolve().parents[1]
class ScriptParser(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.scripts: list[dict[str, str | None]] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == "script":
self.scripts.append(dict(attrs))
def test_bootstrap_is_local_and_deferred() -> None:
index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
assert "cdn.jsdelivr.net/npm/bootstrap" not in index_html
assert '<script defer src="vendor/bootstrap/bootstrap.bundle.min.js"></script>' in index_html
assert (PROJECT_ROOT / "webui" / "vendor" / "bootstrap" / "bootstrap.bundle.min.js").is_file()
def test_classic_startup_scripts_are_deferred() -> None:
index_html = (PROJECT_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
parser = ScriptParser()
parser.feed(index_html)
blocking_scripts = [
script["src"]
for script in parser.scripts
if script.get("src")
and script.get("type") != "module"
and "defer" not in script
and "async" not in script
]
assert blocking_scripts == []

View file

@ -25,6 +25,7 @@
- Component HTML loaded by the shared loader may include `<title>`, module scripts, body content, and scoped styles; modal content uses the same loader path.
- Do not bypass WebSocket origin/auth/CSRF assumptions from frontend code.
- Avoid editing vendored files unless intentionally updating the vendor asset.
- Startup scripts in `index.html` must be local and non-parser-blocking: use ES modules, `defer`, or `async` for every script with `src`.
- Rubik (`--font-family-main`) is the default WebUI text and control font; use the code/mono font tokens only for code, logs, paths, and fixed-width data.
- Hover, focus, and active border treatments should follow existing neutral border/background patterns; avoid hard-coded blue border highlights unless matching an established specialized surface.

View file

@ -23,7 +23,7 @@
<!-- Flatpickr for datetime picker -->
<link rel="stylesheet" href="vendor/flatpickr/flatpickr.min.css">
<script src="vendor/flatpickr/flatpickr.min.js"></script>
<script defer src="vendor/flatpickr/flatpickr.min.js"></script>
<script>
globalThis.safeCall = function (name, ...args) {
@ -34,15 +34,14 @@
<script type="module" src="index.js"></script>
<!-- Bootstrap JS (only for logic, importing bundled CSS => UI conflicts) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>
<script defer src="vendor/bootstrap/bootstrap.bundle.min.js"></script>
<!-- Then load Alpine.js -->
<!-- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script> -->
<script defer src="vendor/ace-min/ace.js"></script>
<script defer src="vendor/qrcode.min.js"></script>
<script type="module" src="js/initFw.js"></script>
<script src="vendor/ace-min/ace.js"></script>
<link href="vendor/ace-min/ace.min.css" rel="stylesheet">
<!-- KaTeX CSS -->
<link rel="stylesheet" href="vendor/katex/katex.min.css" crossorigin="anonymous">
@ -51,9 +50,6 @@
<script defer src="vendor/katex/katex.min.js" crossorigin="anonymous"></script>
<script defer src="vendor/katex/katex.auto-render.min.js" crossorigin="anonymous"></script>
<!-- QR Code Library -->
<script src="vendor/qrcode.min.js"></script>
<!-- Google Icons -->
<link rel="stylesheet" href="vendor/google/google-icons.css" />

View file

@ -35,6 +35,7 @@ Direct child DOX files:
| [_ace/AGENTS.md](_ace/AGENTS.md) | Minimal Ace editor subset used by core editor surfaces. |
| [ace-min/AGENTS.md](ace-min/AGENTS.md) | Full minified Ace distribution and supporting assets. |
| [alpine/AGENTS.md](alpine/AGENTS.md) | Vendored Alpine.js core and collapse plugin. |
| [bootstrap/AGENTS.md](bootstrap/AGENTS.md) | Vendored Bootstrap JavaScript bundle for collapse and tooltip behavior. |
| [dompurify/AGENTS.md](dompurify/AGENTS.md) | Vendored DOMPurify sanitizer module. |
| [flatpickr/AGENTS.md](flatpickr/AGENTS.md) | Vendored Flatpickr date/time picker assets. |
| [google/AGENTS.md](google/AGENTS.md) | Vendored Google Material Symbols font and stylesheet. |

27
webui/vendor/bootstrap/AGENTS.md vendored Normal file
View file

@ -0,0 +1,27 @@
# Bootstrap Vendor DOX
## Purpose
- Own the vendored Bootstrap JavaScript bundle used for collapse and tooltip behavior.
## Ownership
- `bootstrap.bundle.min.js` owns Bootstrap runtime behavior.
## Local Contracts
- Do not hand-edit the minified vendor file for application behavior.
- Keep `webui/index.html` synchronized with the local bundle path.
- Bootstrap must be served locally and loaded with `defer` so startup does not wait on an external parser-blocking script.
## Work Guidance
- Replace from a clean upstream release when updating.
## Verification
- Smoke-test sidebar collapse controls and tooltips after changes.
## Child DOX Index
No child DOX files.

File diff suppressed because one or more lines are too long