perf: keep globe textures at full resolution, switch to WebP - #473
Draft
alukach wants to merge 3 commits into
Draft
perf: keep globe textures at full resolution, switch to WebP#473alukach wants to merge 3 commits into
alukach wants to merge 3 commits into
Conversation
Both WebGL globe textures shipped at 4096x2048 for a canvas that renders
at most ~700px, and the scene is reduced to a 1-bit black-and-white image
by the 8x8 Bayer dither pass (DitherShader.ts) before it ever reaches the
screen. The clouds layer is composited at 0.3 opacity on top of that.
None of that detail survives, so it was 6.2MB of bytes with no visible
output. On a real-throttled 4G profile clouds.png alone took 40.1s to
download and starved the render-blocking stylesheet that gates first paint.
clouds.png 4918 KiB -> 384 KiB (grayscale+alpha preserved)
earth-blue-marble.jpg 1429 KiB -> 89 KiB
total 6347 KiB -> 473 KiB (93% reduction)
Filenames and formats are unchanged, so no application code changes.
Regenerated with:
magick public/img/clouds.png -resize 1024x -strip public/img/clouds.png
magick public/img/earth-blue-marble.jpg -resize 1024x -strip -quality 82 \
public/img/earth-blue-marble.jpg
Refs #471
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Claude finished @alukach's task in 3m 54s —— View job Review
✅ No blocking issues — safe to merge.
|
This was referenced Aug 3, 2026
The two `<link rel="preload">` hints pulled 6.5MB of WebGL globe textures
into the critical window, ahead of the render-blocking stylesheet that
gates first paint. On a real-throttled 4G profile that stylesheet was
requested at 682ms but did not arrive until 5901ms.
The preloads had no upside to trade against that: LiveGlobe is
dynamic({ ssr: false }), so it mounts after hydration and requests the
textures itself regardless.
Measured with Lighthouse 13.4.1 (--throttling-method=devtools, mobile):
FCP 6.1s -> 5.3s
LCP 6.1s -> 5.3s
Refs #471
Replaces the 1024x512 downsize, which was visibly blurry. The reasoning behind it was wrong: the Bayer pass thresholds per *screen* pixel, and the tone curve before it (pow(luma, 0.4) then smoothstep) deliberately pulls detail out of dark regions. Ordered dithering renders input detail as dot density rather than discarding it, so blurring the input blurs the output. clouds.png was also already Bilevel, so downsampling turned crisp 1-bit edges into grey mush. Keeping full resolution and changing the codec instead: clouds 4916 KiB -> 922 KiB (4096x2048 kept) earth-blue-marble 1428 KiB -> 490 KiB (4096x2048 kept) total 6344 KiB -> 1412 KiB (78% reduction) Fidelity against the originals, measured as RMSE: full-res WebP 1.4% (clouds mask), 1.3% (earth) 1024 downsize 7.1% (clouds mask) Nearly all of clouds' weight was its alpha channel: the colour channel held only 2 unique values and was white exactly wherever alpha > 0 (verified, RMSE 0), so it carried no information the mask doesn't. Storing the mask as plain greyscale and binding it via alphaMap is arithmetically identical to the old map binding — map did `a *= texture.a` with rgb pinned to white, alphaMap does `a *= texture.g` — and lets it compress as an ordinary greyscale image. Refs #471
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #471. Stacked on #472 — review that one first; this PR's base is
perf/drop-globe-texture-preload, so it only shows the texture changes.What
Keeps both textures at 4096×2048 and changes the codec instead.
Why the downsize was wrong
My original justification was that
DitherShader.tsreduces the scene to 1-bit black-and-white, so texture detail couldn't matter. That's backwards:vUv * resolution), not texture space. Ordered dithering renders input detail as dot density — it doesn't discard it. Blur the input, blur the output.pow(luma, 0.4)thensmoothstep(0.05, 0.6, luma)before thresholding, which deliberately pulls detail out of dark regions. It amplifies exactly the detail I assumed was being thrown away.clouds.pngwas alreadyBilevel— 1-bit colour. Downsampling it turned crisp 1-bit edges into grey mush, which is the visible blur.Measured, as RMSE against the originals:
5× better fidelity, and still removes 78% of the bytes.
The clouds change needs a second look
Nearly all of clouds' weight was its alpha channel, not its pixels — the colour channel held only 2 unique values, and I verified it is white exactly wherever alpha > 0 (RMSE 0, not approximately zero). So the RGB channel carried no information the alpha mask doesn't.
Storing just the mask as ordinary greyscale lets it compress properly, and binding it via
alphaMapinstead ofmapis arithmetically identical:map_fragmentdoesdiffuseColor *= texture→rgb *= white,a *= texture.aalphamap_fragmentdoesdiffuseColor.a *= texture.gBoth leave
rgb = whiteanda = 0.3 × mask. Confirmed against three.js 0.183.2'salphamap_fragment.glsl.js, which samples.g— and the mask is greyscale, sogis the mask.Why not 1-bit, as suggested
Worth recording, since it was the natural next idea. Pre-thresholding the earth texture to 1-bit would make things worse, twice over:
The instinct was right though — the wasted bits were real, just not in the bit depth. They were in a redundant RGB channel and an inefficient codec.
Dropping colour from the earth texture also isn't worth it: it saves only 68 KiB, and ImageMagick's Rec709 grey weights don't match the shader's Rec601
dot(color.rgb, vec3(0.299, 0.587, 0.114)), which would shift ocean brightness and change the dither density.Regenerated with
Verification
npm run type-check— 14 errors, identical to the baseline onmaingrep— no remaining references toclouds.png/earth-blue-marble.jpganywhereclouds.webpvs the original alpha mask: RMSE 1.4% at full 4096×2048alphaMapswap is exact, but I can't run a browser here, and the last version of this PR is exactly why that matters.🤖 Generated with Claude Code