Skip to content

[nightshift] perf-regression: performance analysis for quarzite #6

@nightshift-micr

Description

@nightshift-micr

Performance Regression Analysis — micr-dev/quarzite

Automated by Nightshift v3 (GLM 5.1).

Analyzed 20 JS files (~2,082 lines), 2 CSS files (~17KB), and 56MB of assets.


P1 — Massive unoptimized image assets (10MB+)

Files: assets/crossover.png (10.2MB), assets/farhan.png (6.2MB), assets/brainrotcat.png (4.4MB), assets/nana.png (3MB)

27 gallery images total, many exceeding 1MB as uncompressed PNG. The largest (crossover.png) is 10MB — loading this single image on a mobile connection takes ~30s.

Impact: 30MB+ of images fetched on a gallery page. Even with loading=lazy, visible images still block initial paint.

Recommendation:

  • Convert gallery images to WebP (typically 60-80% smaller) or AVIF
  • Use responsive <picture> / srcset to serve smaller variants for mobile
  • Run sharp or squosh to resize images beyond their display size
  • Consider a CDN with automatic image optimization (Netlify supports this natively)

P2 — cache: "no-store" prevents caching of gallery data

File: js/gallery-shared.js (line 158)

response = await fetch(url, { cache: "no-store" });

Forces a full network round-trip for data/gallery.json on every page load, even though this file changes rarely. The browser cache is bypassed entirely.

Impact: Unnecessary network latency on revisits.

Recommendation: Use default caching (remove cache: "no-store"), or use cache: "default" with a cache-busting query param when the data actually changes.


P2 — Background SVG generated at runtime on every page load

File: js/background.js (lines 102-151)

A complex SVG with 4 linear gradients, a drop-shadow filter, and 4 polygons is constructed in JavaScript on every page load. The theme (#8621E7) and tile size (10px) are constants — this SVG never changes.

Impact: Unnecessary JS execution + string concatenation + URI encoding on every load.

Recommendation: Pre-generate the SVG as a static file and reference it via CSS background-image: url(pattern.svg). If runtime theming is needed, cache the data URL after first generation.


P2 — New Audio() object created on every sound play

File: js/sounds.js (lines 12-18)

function play(name) {
    const a = new Audio(base + f);
    a.play().catch(() => {});
}

Creates a fresh Audio element per play call. This prevents the browser from preloading audio, adds GC pressure, and introduces playback latency (the audio file must be fetched/decoded each time).

Impact: 100-300ms delay before sounds play, audio files re-fetched repeatedly.

Recommendation: Create Audio objects once and reuse them. Preload on first interaction:

const cache = {};
function play(name) {
    if (!cache[name]) cache[name] = new Audio(base + files[name]);
    cache[name].currentTime = 0;
    cache[name].volume = (window.AppVolume ?? 100) / 100;
    cache[name].play().catch(() => {});
}

P2 — Layout thrashing in drag handler

File: js/drag.js (line 20)

const style = getComputedStyle(el);
startLeft = px(style.left);
startTop = px(style.top);

getComputedStyle forces the browser to flush the style calculation queue. Called on every mousedown start, it triggers a synchronous style recalculation. During active drags, move() reads el.offsetWidth / container.clientWidth (line 49-50) which also force layout.

Impact: Jank during window drag, especially with many DOM elements.

Recommendation: Cache left/top from inline style or a data attribute instead of reading computed style. Throttle layout reads in move().


P2 — Triple getComputedStyle in image viewer resize

File: js/gallery.js (lines 158-165)

adjustToImageWidth() calls getComputedStyle three times for different elements (window body, figure). Each call may trigger a separate style recalculation.

Recommendation: Batch all style reads before any style writes to avoid forced reflows. Use requestAnimationFrame (already used for the call, but not inside the function).


P3 — Duplicate z-index tracking across modules

Files: js/gallery.js (line 25-32) and js/app.js (line 61)

Both files track z-index independently — gallery.js scans all windows with getComputedStyle on every click, while app.js increments a shared counter. The gallery's bringToFront() is slower (O(n) with n = window count) and doesn't coordinate with app.js's counter.

Recommendation: Use a single shared z-index counter (e.g., window.AppZIndex) and increment it from both modules.


P3 — Date objects created during sort

File: js/gallery-shared.js (lines 147-153)

sortImagesByDate creates a new Date object for every comparison. For 27 items, this means ~130 Date object allocations during sort.

Impact: Minor, but unnecessary allocation.

Recommendation: Parse dates once and sort by pre-computed timestamps, or compare ISO date strings directly if format permits.


Summary

Severity Count Key Issues
P1 1 30MB+ of unoptimized PNG images
P2 5 No-store caching, runtime SVG, Audio GC, layout thrashing
P3 2 Duplicate z-index, sort allocations
Total 8

Highest impact fix: Image optimization (P1) would reduce total asset size by ~70-80% with WebP conversion alone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    dayshift/failedManaged by hermes-dayshift-glm

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions