perf(assets): re-export the background at 2560x1440 - #181
Conversation
PR #169 recompressed this file but left it at 3840x2160, and the review pointed out that the dimensions are where the memory actually is: Chromium decodes to the intrinsic size, so every window paid for a 4K bitmap no matter how small it was. The decoded ceiling drops from 31.6 MiB (3840*2160*4) to 14.1 MiB (2560*1440*4). Re-exported from the pre-#169 quality-99 original recovered from git history rather than from the shipped quality-76 file, which avoids a second generation of JPEG loss. Lanczos in linear light, then quality 92 with 4:4:4 chroma. Measured at 40.49 dB PSNR against an uncompressed Lanczos downscale of that original, matching the 40.53 dB bar #169 held itself to. File size 1,581,834 -> 1,381,147 bytes. 2560x1440 lands 1:1 on a maximized window on a 1440p panel and still has headroom for a 1280-wide window at devicePixelRatio 2. A maximized window on a 4K panel upscales 1.5x, which sits under the 2px backdrop-blur the root div already applies over this image.
Zaldaryon
left a comment
There was a problem hiding this comment.
Checked out 0078a4a and verified the asset directly rather than trusting the description's numbers: the shipped file is 1,381,147 bytes and decodes to 2560x1440, both exact matches to the PR body. Confirmed it's the only background asset in the tree and the only thing src/renderer/src/styles.css's --background-image-image-vs points to, referenced by exactly the three elements the description names (App.tsx's root div and loader overlay, PopupDialogPanel), all through the same bg-image-vs utility and bg-cover, so the decode-cache math in the description applies to a single shared entry as claimed.
No code changed, so typecheck/lint/format/test:coverage are identical to dev. The reasoning for 2560x1440 over the original 3840x2160 is laid out with real arithmetic (device pixel budgets, PSNR/DSSIM at matched resolution) rather than a guess, and honestly flags the one case it doesn't cover (a maximized 4K panel) instead of hiding the trade-off.
No blocking findings. Approving.
Summary
PR #169 compressed
src/renderer/src/assets/background.jpgfrom 4.35 MiB to 1.51 MiB and left the dimensions at 3840x2160 on purpose. The review of that PR pointed out what the compression did not fix: file size is not what the background costs at runtime. Chromium decodes an image at its intrinsic size, so the app held a 4K bitmap in the image decode cache regardless of how big the window actually was. The window measured during that review was 1911x1052 at devicePixelRatio 1, roughly a quarter of the pixels being decoded. This PR re-exports the asset at 2560x1440 and changes nothing else.The numbers:
That is 17.58 MiB off the decode ceiling and 196 KiB off the packaged size. The decode figure is the arithmetic bound implied by the intrinsic dimensions, not a runtime measurement, so read it as a ceiling rather than as a number you would see in a profiler. Three elements reference the asset through the
bg-image-vsutility (the root div inApp.tsx, the loader overlay in the same file, andPopupDialogPanel), all of them withbg-cover, so they share one entry in the decode cache and the ceiling is per-image, not per-element.Why 2560x1440
background-size: coveron a 16:9 image in a 16:9 window means the scale factor is just window device pixels divided by image width, so the question is what the largest realistic window is in device pixels.createWindowinsrc/main/index.tsopens at 1280x720, clamps to a 1024x600 minimum, setsfullscreenable: false, and restores whatever size and maximized state the user last had. So the window can be anything from 1024x600 up to a maximized window on the user's largest display, multiplied by their device pixel ratio.2560x1440 covers a maximized window on a 1440p panel exactly 1:1, and it covers a 1280-wide window at devicePixelRatio 2, which is the common Windows-at-150%-or-200% case on a laptop panel. What it does not cover is a maximized window on a 4K panel, at either DPR 1 or DPR 2, where it upscales 1.5x. I went with that trade knowingly. The root div paints this image under
before:...backdrop-blur-[2px]plus abg-zinc-950/15tint, so the background is already being blurred and dimmed before anyone sees it, and a 1.5x upscale of a soft game scene under a 2px blur is not something I can pick out side by side. The 31.6 MiB decode, on the other hand, was paid by every user on every screen size, including the 1024x600 minimum.Shipping 4K for the minority who would benefit means the majority pays for pixels their monitor cannot show. If someone wants both, the right fix is
image-set()with two files, which costs a second asset in the bundle and is worth doing only if 4K users actually complain.How the file was produced
Source is not the currently shipped file.
git logon the asset shows the pre-#169 version at 68c7636 is a quality-99 JPEG, and downscaling from that instead of from the shipped quality-76 file avoids stacking a second generation of JPEG loss on top of the first. The difference is measurable: a 1440p downscale of the shipped file scores 41.94 dB PSNR against a 1440p downscale of the quality-99 original, so the artifacts from the first compression pass do survive the resize.The
-colorspace RGBround trip does the resize in linear light. 4:4:4 chroma rather than 4:2:0 because on this particular image it wins on both axes: at a matched file size around 675 KB, 4:4:4 at quality 70 scores 35.93 dB where 4:2:0 at quality 82 scores 35.09 dB. Quality 92 was chosen to land on the same quality bar #169 set for itself rather than to quietly cash in a quality drop as a memory win.Quality, measured
PSNR across different resolutions is meaningless, so here are two same-resolution comparisons instead.
At a maximized 1440p window, against an uncompressed Lanczos downscale of the quality-99 original as the reference: what ships today (the 4K file, downscaled by the browser) scores 41.94 dB, the new file scores 40.49 dB.
At a maximized 4K window, against the quality-99 original: what ships today scores 40.53 dB, which reproduces the 40.53 dB figure in #169's description and confirms the baseline is the same file. The new file, upscaled back to 4K, scores 38.65 dB.
So 4K users lose about 1.9 dB and everyone at 1440p or below loses about 1.4 dB, both landing in territory usually called visually lossless, and both further softened by the blur that sits over this image anyway. DSSIM tracks the same ordering if you prefer a perceptual metric: 1109 vs 777 at 1440p, 1681 vs 1164 at 4K.
Type
Checklist
dev, notmain.npm run typecheckpasses.npm run lint:cipasses (0 errors, 17 pre-existingreact-hooks/exhaustive-depswarnings, untouched by this PR).npm run format:checkpasses.npm run test:coveragepasses, coverage at or above the floor invitest.config.ts(lines 92.48% vs 89%, statements 90.79% vs 87%, functions 89% vs 85%, branches 87.83% vs 85%).npm run build:unpackpasses.Testing
107 test files, 1220 passing, 2 skipped, the same pre-existing skips as on
dev. No test changed, and none could have caught this: nothing in the suite asserts on the asset's dimensions or file size, and jsdom has no image decoder.The check that matters here is visual, so I ran the dev build and inspected the live renderer over CDP. The window came up at 1911x1052 at devicePixelRatio 1, the same geometry the #169 review measured. Computed style on the root div resolves to
url("http://localhost:5173/src/assets/background.jpg")withbackground-size: coverandbackground-position: 50% 50%, the fetch returns 200 with 1,381,147 bytes, andcreateImageBitmapon those bytes reports 2560x1440, so the new asset is the one being painted and there is no 404 or fallback. A full-window screenshot shows the background rendering normally: sky gradient, foliage, the blur and tint over it, no banding or visible artifacts in the sky, which is where JPEG would show first on a scene like this.Not covered: I have no 4K panel here, so the 1.5x upscale case is argued from the metrics above rather than from having looked at it. If a reviewer has one, that is the thing worth a second pair of eyes.
Related issues
None. This is the follow-up the #169 review asked for.