Performance: stop iPhone freezes (blur over live map, runaway animation, eager charts)
Publish Container / publish (push) Successful in 2m57s

Main app
- No backdrop-filter over the live map, animated sky, or sticky headings (it re-blurs every
  frame; on iPhone that is a classic stutter/lock-up); top/bottom bars keep a light 8px blur
- Sky effect only draws while the hero is on screen and the Now tab is active, ~30fps cap,
  fewer particles on phones; fixed a stale animation-frame id that could stop restarts

Dashboard
- Charts are built only when the Charts tab is opened (was ~11 charts on every page load),
  and the 2-minute refresh no longer rebuilds them in the background
- On phones: max 360 points per chart (was 720), no entry animation, device pixel ratio capped at 2
- Lighter header blur, none on small buttons/cards

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 5.5 committed 2026-10-15 15:29:14 -04:00
1 parent dde59e3865
commit fda8dca435
5 files changed
+53 -13

No files matched your search

+16 -7
View File
@@ -53,7 +53,10 @@ function skyFor(code, isDay, stationRaining) {
return isDay ? 'clear-day' : 'clear-night';
}
const fx = { raf: 0, kind: null, parts: [], w: 0, h: 0, flash: 0 };
const fx = { raf: 0, kind: null, parts: [], w: 0, h: 0, flash: 0, last: 0, onScreen: true };
const PHONE = window.matchMedia('(max-width: 640px)').matches;
const fxShouldRun = () => fx.parts.length && !REDUCED && !document.hidden && fx.onScreen && document.body.dataset.tab === 'conditions';
function fxKick() { if (!fx.raf && fxShouldRun()) fx.raf = requestAnimationFrame(fxFrame); }
function fxResize(canvas) {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
fx.w = canvas.clientWidth; fx.h = canvas.clientHeight;
@@ -61,7 +64,8 @@ function fxResize(canvas) {
canvas.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0);
}
function fxSeed(kind) {
const n = kind === 'storm' ? 130 : kind === 'rain' ? 85 : kind === 'snow' ? 70 : kind === 'clear-night' ? 55 : 0;
const base = kind === 'storm' ? 130 : kind === 'rain' ? 85 : kind === 'snow' ? 70 : kind === 'clear-night' ? 55 : 0;
const n = Math.round(base * (PHONE ? 0.55 : 1));
fx.parts = Array.from({ length: n }, () => ({
x: Math.random() * fx.w, y: Math.random() * fx.h,
v: kind === 'snow' ? 0.4 + Math.random() * 0.9 : kind === 'clear-night' ? 0 : 6 + Math.random() * 7,
@@ -70,8 +74,11 @@ function fxSeed(kind) {
}));
}
function fxFrame(t) {
fx.raf = 0;
const canvas = $('sky-fx');
if (!canvas) return;
if (!canvas || !fxShouldRun()) return; // hidden tab / scrolled away / app in background: stop drawing entirely
if (t - fx.last < 33) { fx.raf = requestAnimationFrame(fxFrame); return; } // cap at ~30 fps
fx.last = t;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, fx.w, fx.h);
const kind = fx.kind;
@@ -100,21 +107,23 @@ function fxFrame(t) {
ctx.beginPath(); ctx.arc(p.x, p.y * 0.8, p.r, 0, Math.PI * 2); ctx.fill();
}
}
if (!REDUCED && !document.hidden) fx.raf = requestAnimationFrame(fxFrame);
fx.raf = requestAnimationFrame(fxFrame);
}
function setSky(kind) {
const hero = $('hero'), canvas = $('sky-fx');
if (!hero || !canvas) return;
hero.dataset.sky = kind;
cancelAnimationFrame(fx.raf);
fx.raf = 0;
fx.kind = kind;
fxResize(canvas);
fxSeed(kind);
if (fx.parts.length) fxFrame(performance.now());
if (fx.parts.length) fxKick();
else canvas.getContext('2d').clearRect(0, 0, fx.w, fx.h);
}
window.addEventListener('resize', () => { const c = $('sky-fx'); if (c && fx.kind) { fxResize(c); fxSeed(fx.kind); } });
document.addEventListener('visibilitychange', () => { if (!document.hidden && fx.parts.length && !REDUCED) { cancelAnimationFrame(fx.raf); fxFrame(performance.now()); } });
document.addEventListener('visibilitychange', fxKick);
if ('IntersectionObserver' in window && $('hero')) new IntersectionObserver((es) => { fx.onScreen = es[0].isIntersecting; fxKick(); }).observe($('hero'));
// ── Nowcast: next 2 hours from 15-minute precipitation ───────────────────────
function nowcastSlots(data) {
@@ -459,7 +468,7 @@ window.AppUI = {
centerMap,
refreshGeoStatus: () => GeoStatus.refresh(),
onAir(air) { renderAqi(air); },
refreshFx() { const c = $('sky-fx'); if (c && fx.kind) { cancelAnimationFrame(fx.raf); fxResize(c); fxSeed(fx.kind); if (fx.parts.length) fxFrame(performance.now()); } },
refreshFx() { const c = $('sky-fx'); if (c && fx.kind) { cancelAnimationFrame(fx.raf); fx.raf = 0; fxResize(c); fxSeed(fx.kind); fxKick(); } },
onStation() { syncTopbarLocation(); renderStationContext(); },
};