From 328f8ab0d1b84bf728d2192498e8a24895555fc0 Mon Sep 17 00:00:00 2001 From: eleith Date: Mon, 2 Nov 2026 15:29:39 -0800 Subject: [PATCH] perf(web): inline initial booking styles --- .changeset/inline-booking-styles.md | 5 +++++ apps/web/e2e/critical-css.e2e.ts | 34 +++++++++++++++++++++++++++++ apps/web/vite.config.ts | 3 ++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .changeset/inline-booking-styles.md create mode 100644 apps/web/e2e/critical-css.e2e.ts diff --git a/.changeset/inline-booking-styles.md b/.changeset/inline-booking-styles.md new file mode 100644 index 00000000..115efba1 --- /dev/null +++ b/.changeset/inline-booking-styles.md @@ -0,0 +1,5 @@ +--- +'@when/web': patch +--- + +Raise the per-stylesheet inlining limit so booking and rescheduling include their initial styles in the HTML rather than waiting for a render-blocking CSS request. This applies across the app, trading larger HTML responses for fewer separate stylesheet requests; styles above the limit remain external. diff --git a/apps/web/e2e/critical-css.e2e.ts b/apps/web/e2e/critical-css.e2e.ts new file mode 100644 index 00000000..d3bf5df7 --- /dev/null +++ b/apps/web/e2e/critical-css.e2e.ts @@ -0,0 +1,34 @@ +import { expect, test } from './support/test.ts'; +import { seedAppointment } from './support/seed.ts'; + +test.use({ javaScriptEnabled: false }); + +for (const viewport of [ + { name: 'mobile', width: 390, height: 844 }, + { name: 'desktop', width: 1280, height: 900 } +]) { + test(`booking and rescheduling have inline initial styles on ${viewport.name}`, async ({ + page + }) => { + await page.setViewportSize(viewport); + // The first render must not depend on an external stylesheet or hydration. + await page.route('**/_app/immutable/assets/*.css', (route) => route.abort()); + const appointment = await seedAppointment(); + const paths = [ + '/schedule/chat', + `/appointment/${appointment.id}/reschedule?token=${appointment.cancel_token}` + ]; + + for (const path of paths) { + const response = await page.goto(path); + expect(response?.status()).toBe(200); + await expect(page.getByRole('heading', { name: 'Step 1 of 3: Pick a day' })).toBeVisible(); + await expect(page.locator('head link[rel="stylesheet"]:not([disabled])')).toHaveCount(0); + await expect(page.locator('.card-stage')).toHaveCSS('display', 'flex'); + await expect(page.locator('.page-banner')).toHaveCSS( + 'display', + viewport.name === 'mobile' ? 'flex' : 'none' + ); + } + }); +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 5941cd1d..a44b9289 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -24,7 +24,8 @@ export default defineConfig(({ command }) => { filename.split(/[/\\]/).includes('node_modules') ? undefined : true }, adapter: adapter(), - inlineStyleThreshold: 10240, + // Include the booking wizard's CSS in the initial HTML, avoiding a blocking request. + inlineStyleThreshold: 32 * 1024, csp: { directives: { 'default-src': ['self'], -- 2.54.0