# Frontend Styling & Performance Improvements *Analysis date: 2026-04-01* This document captures observations and improvement recommendations for the trx-rs web frontend (`trx-frontend-http`). The frontend is a single-page application served as embedded static assets (gzip-compressed with ETag caching) from the Actix-Web server. ## Current asset inventory | File | Lines | Size | |------|------:|-----:| | `style.css` | 5,318 | 144 KB | | `app.js` | 8,427 | 306 KB | | `map-core.js` | 3,483 | 127 KB | | `screenshot.js` | 261 | 10 KB | | `index.html` | 1,564 | 96 KB | | `webgl-renderer.js` | 526 | 20 KB | | `decode-history-worker.js` | 176 | 8 KB | | `leaflet-ais-tracksymbol.js` | 120 | 8 KB | | 15 plugin scripts | 7,360 | 304 KB | | **Total** | **~27,000** | **~1 MB** | All assets are pre-compressed with `flate2` (gzip, `Compression::best()`) and served with `ETag` + `If-None-Match` support for conditional requests. The Actix `Compress` middleware handles dynamic responses. --- ## 1. CSS observations ### 1.1 Monolithic stylesheet (P1) `style.css` is a single 5,318-line file covering every tab, theme, responsive breakpoint, map overlay, decoder UI, scheduler, recorder, and settings panel. Browsers must parse the entire stylesheet before first paint even though most users only interact with 1-2 tabs at a time. **Recommendations:** - Split into logical partitions: `base.css` (variables, reset, layout), `tabs/*.css` (per-tab styles), `themes/*.css`. The server can concatenate and compress at build time. - At minimum, move the theme colour blocks (lines 3770-5318, ~1,550 lines / 29% of the file) into a separate `themes.css` loaded asynchronously after initial paint, since the default theme is already in `:root`. - Consider using `@layer` (CSS Cascade Layers) to manage specificity between base, component, and theme styles, eliminating the need for `!important` (currently 21 occurrences). ### 1.2 `backdrop-filter` overuse (P1) There are 26 `backdrop-filter` declarations (13 pairs with `-webkit-` prefix). `backdrop-filter: blur()` is one of the most expensive CSS properties -- it forces the browser to composite, rasterize, and blur everything behind the element on every frame. Affected areas: tab bar, controls tray, frequency overlay, modals, connection banner, bottom nav, neon-disco theme overlay. **Recommendations:** - Remove `backdrop-filter` from elements that are always opaque or rarely overlap dynamic content (e.g. bottom tab bar over static background). - For the spectrum/waterfall overlay controls, use a solid semi-transparent `background` instead of blur -- the visual difference is negligible on a dark spectrogram. - Where blur is desired (modals), use `will-change: backdrop-filter` and keep blur radius low (4-6px instead of 12-18px). Larger radii are proportionally more expensive. - Gate expensive blur behind a `@media (prefers-reduced-motion: no-preference)` query or a `[data-effects="full"]` attribute so low-end devices can opt out. ### 1.3 `color-mix()` usage (P2) 184 occurrences of `color-mix(in srgb, ...)` throughout the stylesheet. While `color-mix` is well-supported in modern browsers, each call is resolved at computed-value time. Repeated identical mixes (e.g. button hover states repeated across themes) add unnecessary style recalculation cost. **Recommendations:** - Pre-compute frequently used mixes as CSS custom properties in the theme blocks (e.g. `--btn-hover-bg`, `--btn-active-bg`). - This reduces computed-value work and also makes the palette more explicit and maintainable. ### 1.4 Theme system duplication (P2) Each of the 10 colour themes repeats ~28 variable declarations for both dark and light mode (560 variable declarations total). The theme blocks span lines 3770-5318 (29% of the entire stylesheet). **Recommendations:** - Move themes to a separate file loaded after first paint (the default `:root` theme is always available). - Consider generating theme CSS from a data source (JSON/TOML) at build time to reduce manual duplication. - Use `color-scheme` and `light-dark()` (CSS Color Level 5) to collapse the dark/light pairs where values differ only in lightness. ### 1.5 Transitions on non-essential properties (P3) 25 `transition` declarations, several targeting `background`, `border-color`, and `box-shadow` simultaneously. Multi-property transitions on buttons and inputs cause style recalculation on hover/focus for every such element. **Recommendations:** - Prefer transitioning only `opacity` and `transform` (GPU-composited). - For colour changes, use `transition: background-color 100ms` rather than the shorthand `background` which also transitions `background-image` and other sub-properties. - Add `will-change: transform` only to elements that are actively animating (currently only 2 occurrences, which is good). ### 1.6 Missing `contain` declarations (P2) Tab content panels, decode history tables, map containers, and spectrum canvases do not use CSS `contain` or `content-visibility`. When a large decode history table updates, the browser recalculates layout for the entire page. **Recommendations:** - Add `contain: content` to inactive tab panels (`[data-tab]:not(.active)`). - Add `content-visibility: auto` with `contain-intrinsic-size` to off-screen panels (decode history, map, statistics). This lets the browser skip rendering for hidden content entirely. - Add `contain: strict` to the spectrum/waterfall canvas containers since their size is fixed and they don't affect sibling layout. --- ## 2. JavaScript observations ### 2.1 Monolithic `app.js` (P1) The main application script is 11,928 lines (428 KB uncompressed). It is loaded synchronously in the HTML `` (via embedded asset), blocking first paint until fully parsed and executed. The 15 plugin scripts add another 7,360 lines. **Recommendations:** - Mark the script tag `defer` or move it to end of `` so HTML parsing completes before script execution. - Split `app.js` into logical modules: `core.js` (SSE, auth, render loop), `spectrum.js`, `map.js`, `decoder.js`, `recorder.js`, `settings.js`. Load non-critical modules lazily when the user navigates to the corresponding tab. - Use ES modules (`type="module"`) for clean dependency management and tree-shaking potential. ### 2.2 DOM query overhead (P2) The codebase contains ~359 `querySelector`/`getElementById` calls, many of which execute on every SSE event (inside `render()`). DOM lookups are not free, especially `querySelector` with compound selectors. **Recommendations:** - Cache DOM references at initialization time (many already are, but the render path still re-queries elements like `document.getElementById("tab-main")`). - Move repeated lookups (e.g. line 3575 `document.getElementById("tab-main")` inside `es.onmessage`) to module-level constants. ### 2.3 `innerHTML` usage (P2) 33 `innerHTML` assignments in `app.js` and 72 across plugin scripts. Each `innerHTML` write forces the browser to: 1. Serialize the old DOM subtree for GC 2. Parse the HTML string 3. Build and insert a new DOM subtree This is both a performance concern (layout thrashing) and a security concern (XSS if any user-controlled data is interpolated without escaping). **Recommendations:** - Replace `innerHTML` with DOM APIs (`createElement`/`appendChild`) or `DocumentFragment` for bulk updates (only 4 `createDocumentFragment` uses currently). - For large lists (decode history, bookmarks, recorder file lists), use a virtualised list pattern that only renders visible rows. - Where `innerHTML` is used to clear a container, prefer `replaceChildren()` (clears children without HTML parsing). ### 2.4 SSE render path efficiency (P2) Every SSE state event triggers `render(update)` which is a ~300-line function touching dozens of DOM elements. The function does not diff -- it unconditionally sets properties even when values have not changed. The string-equality guard (`if (evt.data === lastRendered) return`) is a good optimisation for identical payloads, but when any field changes (e.g. S-meter value), the entire render function runs. **Recommendations:** - Implement field-level diffing: compare individual fields against previous values and only update DOM elements whose backing data changed. - Group updates by tab: if the user is on the "Map" tab, skip render work for "Main" tab elements (meters, frequency display, controls). - Use `scheduleUiFrameJob()` (already exists at line 3685) more aggressively to batch DOM writes into animation frames. ### 2.5 Spectrum/waterfall rendering (P2) The WebGL renderer (`webgl-renderer.js`) is well-implemented with proper shader programs and batched draws. However: - The CSS colour parsing (`parseCssColor`) uses a DOM probe element (appended to body) and `getComputedStyle` as a fallback, which triggers layout. - The colour cache is a simple `Map` with no eviction policy. **Recommendations:** - Parse theme colours once when the theme changes, not on every frame. - Invalidate the `cssColorCache` on theme switch events. ### 2.6 Plugin script loading (P3) All 15 plugin scripts are loaded eagerly in `index.html` regardless of which decoders are active. Plugins like `ais.js`, `vdes.js`, `sat.js`, `sat-scheduler.js`, and `hf-aprs.js` are only relevant for specific use cases. **Recommendations:** - Load plugin scripts on demand when the corresponding decoder or feature is activated. - Use dynamic `import()` if migrated to ES modules, or lazy `