From e6d615beb234a24ad554009ee1757c5271ad1ecd Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Wed, 2 Sep 2026 08:40:22 -0700 Subject: [PATCH 1/6] feat: stop iOS Safari crashing while scrolling the index page by keeping its GPU footprint bounded: every grain frame's WebGL canvas is now released when it scrolls 300px past the viewport and recreated on approach so the page holds two or three GL contexts instead of nine, phones render the grain at a 1280x720 pixel budget instead of the full 3x device ratio, feature sections a viewport or more away drop their app screens from rendering with content-visibility (display none where unsupported) and refit them on the way back so at most a few of the fourteen scaled windows are composited at once, and backdrop filters inside the scaled app shells are turned off on touch devices where the blur is invisible at that size, with the desktop look unchanged --- site/src/components/FeatureSection.astro | 21 +++++++++++++++++ site/src/components/GrainBackdrop.astro | 29 ++++++++++++++++-------- site/src/styles/global.css | 10 ++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/site/src/components/FeatureSection.astro b/site/src/components/FeatureSection.astro index 410aaf8b8..14542b724 100644 --- a/site/src/components/FeatureSection.astro +++ b/site/src/components/FeatureSection.astro @@ -312,6 +312,11 @@ const mockCol = wide ? `lg:col-span-8 ${side === 'left' ? 'lg:-ml-10' : 'lg:-mr- .feat-track { transition: none; } .feat-bar.is-active .feat-fill { animation: none; transform: scaleX(1); } } + /* Parked (a viewport or more away): the screens are not rendered at all. */ + .feat-frame.is-parked .feat-track { content-visibility: hidden; } + @supports not (content-visibility: hidden) { + .feat-frame.is-parked .feat-track { display: none; } + } {!bare && } diff --git a/site/src/styles/global.css b/site/src/styles/global.css index f7c0ca7c8..7cfd8ea66 100644 --- a/site/src/styles/global.css +++ b/site/src/styles/global.css @@ -914,16 +914,31 @@ body { } } +/* clip-path, not the translateZ + mask-image trick: that pairs a forced 3D + layer with a mask buffer per frame, and iOS Safari runs out of GPU memory + with fourteen of them on one page. clip-path clips composited children + (the WebGL canvas, scaled screens) just as well. */ .round-clip { position: relative; overflow: hidden; border-radius: 28px; isolation: isolate; - transform: translateZ(0); - -webkit-mask-image: -webkit-radial-gradient(white, black); - mask-image: radial-gradient(white, black); + clip-path: inset(0 round 28px); } +/* A shell that is off screen (AppShellMock's observer) keeps its pings and + pulses still; a phone with fifteen shells cannot animate them all. */ +[data-stage].is-off *, +[data-stage].is-off *::before, +[data-stage].is-off *::after { + animation-play-state: paused !important; +} + +/* Diagnostics for the phone: ?noshell hides every app shell, ?noanim stops + every animation, ?nogl (GrainBackdrop) skips WebGL. */ +html[data-diag~="noshell"] [data-stage] { display: none !important; } +html[data-diag~="noanim"] *, html[data-diag~="noanim"] *::before, html[data-diag~="noanim"] *::after { animation: none !important; transition: none !important; } + /* The outer clip. clip-path (not just overflow) because the shader canvas carries `contain: strict` and a negative z-index from the library's own layer; this clips it regardless. */ From 3ecb25786aeb33410a1cece0aaaac892d95594d8 Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Wed, 2 Sep 2026 09:00:29 -0700 Subject: [PATCH 4/6] feat: register the app shell's visibility observer under reduced motion too so an off-screen shell still pauses its animations while only the counter ticker stays off, and read the ?noshell, ?noanim and ?nogl phone diagnostics as query parameter names through URLSearchParams instead of matching the raw query string --- site/src/components/AppShellMock.astro | 5 +++-- site/src/components/GrainBackdrop.astro | 2 +- site/src/layouts/Layout.astro | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/components/AppShellMock.astro b/site/src/components/AppShellMock.astro index a04b26f13..8c54d6d8a 100644 --- a/site/src/components/AppShellMock.astro +++ b/site/src/components/AppShellMock.astro @@ -477,7 +477,8 @@ const online = [ }; let onScreen = false; - const sync = () => (onScreen && !document.hidden ? start() : stop()); + // Reduced motion never ticks the counters; the observer still parks the shell. + const sync = () => (onScreen && !document.hidden && !REDUCED ? start() : stop()); new IntersectionObserver((entries) => { onScreen = entries.some((e) => e.isIntersecting); // Off screen, the pings and pulses inside stop too (see global.css). @@ -487,7 +488,7 @@ const online = [ document.addEventListener('visibilitychange', sync); } - if (!REDUCED) document.querySelectorAll('[data-shell]').forEach(live); + document.querySelectorAll('[data-shell]').forEach(live); // The shell is a picture: nothing inside it is a real tab stop. document.querySelectorAll('[data-shell] button, [data-shell] input, [data-shell] a, [data-shell] [tabindex]').forEach((el) => { el.tabIndex = -1; }); diff --git a/site/src/components/GrainBackdrop.astro b/site/src/components/GrainBackdrop.astro index c0b386a21..eeb122508 100644 --- a/site/src/components/GrainBackdrop.astro +++ b/site/src/components/GrainBackdrop.astro @@ -53,7 +53,7 @@ const t = GRAIN_TONES[tone]; // Phones: a 3x canvas per frame is what pushes iOS Safari over its GPU // memory limit while scrolling; 2x grain reads the same at that size. const touch = window.matchMedia('(hover: none)').matches; - const noGl = /[?&]nogl\b/.test(location.search); + const noGl = new URLSearchParams(location.search).has('nogl'); const maxPixels = touch ? 1280 * 720 : 1920 * 1080 * 2; // Every frame gets its own rotation, drift and shape. Sharing one palette diff --git a/site/src/layouts/Layout.astro b/site/src/layouts/Layout.astro index 4a0a23263..bc2e62963 100644 --- a/site/src/layouts/Layout.astro +++ b/site/src/layouts/Layout.astro @@ -143,7 +143,10 @@ const websiteJsonLd = { + {!bare && } From 15d9f226590f3e438a19268c7cc5ee92146006fc Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Wed, 2 Sep 2026 09:08:56 -0700 Subject: [PATCH 5/6] feat: make phones show the same product visuals as desktop: every feature window is the 1280px browser with chrome and app shell at every width, scaled and cropped from the same anchor at the same zoom, with the frame keeping a desktop frame's proportions below lg instead of growing to each mock's content height; the app shell drops its chrome prop and becomes a container query root, and the mocks' responsive classes become @sm/@md/@lg/@xl container variants with viewport units pinned to the 1280x900 app viewport, so a mock renders its desktop state inside the window regardless of the visitor's screen instead of collapsing to phone layouts inside a desktop screenshot --- site/src/components/AnalyticsMock.astro | 24 +++--- site/src/components/AppShellMock.astro | 26 +++--- site/src/components/AutomationsDemo.astro | 8 +- site/src/components/CRMMock.astro | 20 ++--- site/src/components/CampaignBuilderMock.astro | 10 +-- site/src/components/DashboardMock.astro | 16 ++-- site/src/components/DeveloperMock.astro | 28 +++--- site/src/components/FeatureSection.astro | 86 +++++++++---------- site/src/components/FormBuilderMock.astro | 4 +- site/src/components/FormsMock.astro | 36 ++++---- site/src/components/InboxMock.astro | 4 +- site/src/components/IntegrationsMock.astro | 22 ++--- site/src/components/ScheduleMock.astro | 2 +- site/src/components/UniboxMock.astro | 14 +-- site/src/components/WarmupMock.astro | 2 +- 15 files changed, 144 insertions(+), 158 deletions(-) diff --git a/site/src/components/AnalyticsMock.astro b/site/src/components/AnalyticsMock.astro index b47acc59a..489b0a2a5 100644 --- a/site/src/components/AnalyticsMock.astro +++ b/site/src/components/AnalyticsMock.astro @@ -134,11 +134,11 @@ const shareArea = `${shareLine} L ${sPts[sPts.length - 1][0].toFixed(1)} 314 L 4 ---
-
+
Analytics
Deliverability across the workspace -
+
7d @@ -148,13 +148,13 @@ const shareArea = `${shareLine} L ${sPts[sPts.length - 1][0].toFixed(1)} 314 L 4
-
+
{statStrip.map((s) => (
@@ -168,9 +168,9 @@ const shareArea = `${shareLine} L ${sPts[sPts.length - 1][0].toFixed(1)} 314 L 4
-
-
-
+
+
+
Email performance
{series.map((m) => ( @@ -257,9 +257,9 @@ const shareArea = `${shareLine} L ${sPts[sPts.length - 1][0].toFixed(1)} 314 L 4
{c.name} - + {c.sent} sent - + {c.reply} reply
@@ -277,7 +277,7 @@ const shareArea = `${shareLine} L ${sPts[sPts.length - 1][0].toFixed(1)} 314 L 4 {a.verb} {a.email} - + {a.ts}
))} @@ -286,7 +286,7 @@ const shareArea = `${shareLine} L ${sPts[sPts.length - 1][0].toFixed(1)} 314 L 4