From 6e28d91b960118bdf02a84ac585330ce6b26ca07 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 5 Nov 2026 22:19:50 +0800 Subject: [PATCH] Make the editor respond faster on iPads and phones, and pages paint sooner Each change was kept only if removing it measured slower (6x and 10x CPU throttle, slow 4G): - Monaco loads in small steps, so taps during loading stay responsive. - Run, Create File and opening a file start after the tap has painted. - /app prerenders its title bar; signed-out visitors don't wait for Clerk. - PostHog loads once the page is idle; events, early errors and web vitals are held for it. - The stylesheet is inlined into the HTML, and Inter's fallback matches its metrics. - Clerk middleware runs only for /api; locale routing is a next.config rewrite. - The home page holds first paint until its hero window has arrived, so it does not grow after painting. Co-Authored-By: Claude Opus 5.5 --- apps/web/next.config.ts | 28 +++++- apps/web/src/app/[locale]/(public)/page.tsx | 5 +- apps/web/src/app/[locale]/app/page.test.tsx | 47 ++++++++++ apps/web/src/app/[locale]/app/page.tsx | 79 +++++++++++++--- .../app/components/MonacoPseudocodeEditor.tsx | 26 +++++- apps/web/src/app/globals.css | 14 ++- apps/web/src/instrumentation-client.ts | 51 ++++++++-- apps/web/src/lib/afterNextPaint.test.ts | 41 ++++++++ apps/web/src/lib/afterNextPaint.ts | 35 +++++++ apps/web/src/lib/analytics.test.ts | 39 ++++++-- apps/web/src/lib/analytics.ts | 32 ++++++- apps/web/src/lib/analyticsBootstrap.test.ts | 68 ++++++++++++++ apps/web/src/lib/analyticsBootstrap.ts | 93 +++++++++++++++++++ apps/web/src/lib/auth-components.tsx | 15 +++ apps/web/src/lib/clerkSessionHint.test.ts | 17 ++++ apps/web/src/lib/monaco/editing.ts | 26 ++++++ apps/web/src/lib/monaco/font.ts | 12 +++ apps/web/src/lib/monaco/model.ts | 4 + apps/web/src/lib/monaco/navigation.ts | 12 +++ apps/web/src/lib/monaco/overlays.ts | 6 ++ apps/web/src/lib/monaco/quickInput.ts | 3 + apps/web/src/lib/monaco/services.ts | 3 + apps/web/src/lib/monaco/suggestions.ts | 8 ++ apps/web/src/lib/monaco/view.ts | 6 ++ apps/web/src/lib/monacoLocalLoader.ts | 81 ++++++++++++++-- apps/web/src/lib/posthogClient.ts | 24 +++++ apps/web/src/proxy.ts | 50 ++-------- .../src/types/monaco-editor-internals.d.ts | 13 +++ 28 files changed, 749 insertions(+), 89 deletions(-) create mode 100644 apps/web/src/lib/afterNextPaint.test.ts create mode 100644 apps/web/src/lib/afterNextPaint.ts create mode 100644 apps/web/src/lib/analyticsBootstrap.test.ts create mode 100644 apps/web/src/lib/analyticsBootstrap.ts create mode 100644 apps/web/src/lib/clerkSessionHint.test.ts create mode 100644 apps/web/src/lib/monaco/editing.ts create mode 100644 apps/web/src/lib/monaco/font.ts create mode 100644 apps/web/src/lib/monaco/model.ts create mode 100644 apps/web/src/lib/monaco/navigation.ts create mode 100644 apps/web/src/lib/monaco/overlays.ts create mode 100644 apps/web/src/lib/monaco/quickInput.ts create mode 100644 apps/web/src/lib/monaco/services.ts create mode 100644 apps/web/src/lib/monaco/suggestions.ts create mode 100644 apps/web/src/lib/monaco/view.ts create mode 100644 apps/web/src/lib/posthogClient.ts diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 39d7cd9..77e2db6 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -68,16 +68,42 @@ const nextConfig: NextConfig = { env: { NEXT_PUBLIC_POSTHOG_ENABLED: analyticsEnabled ? "1" : "", }, + experimental: { + // Inline the stylesheet (about 15 KB compressed) into the HTML, so first paint does not wait for + // a separate render-blocking CSS request. Client-side navigations still load it as a file. + inlineCss: true, + }, images: { unoptimized: true, }, poweredByHeader: false, async redirects() { // The manual's content now lives in the docs as the pseudocode reference. - return ["/manual", "/app/manual"].flatMap((source) => [ + const manual = ["/manual", "/app/manual"].flatMap((source) => [ { source, destination: "/docs", permanent: true }, { source: `/:locale(en|zh)${source}`, destination: "/:locale/docs", permanent: true }, ]); + // English is served at the bare path, so `/en/...` redirects there and every page has one URL. + const englishPrefix = [ + { source: "/en", destination: "/", permanent: true }, + { source: "/en/:path*", destination: "/:path*", permanent: true }, + ]; + return [...manual, ...englishPrefix]; + }, + async rewrites() { + // English pages live under app/[locale] as `/en/...`; the bare path is rewritten there. These run + // in Vercel's routing layer, so cached pages are served from the CDN without invoking the proxy + // (middleware). `afterFiles` runs after public files and static routes (robots.txt, icons, + // /api, /_next) but before dynamic routes, so `/docs` reaches `/en/docs` rather than + // `[locale]` = "docs". + return { + beforeFiles: [], + afterFiles: [ + { source: "/", destination: "/en" }, + { source: "/:path((?!(?:en|zh|api|_next)(?:/|$)).+)", destination: "/en/:path" }, + ], + fallback: [], + }; }, async headers() { const immutableAssetHeaders = [ diff --git a/apps/web/src/app/[locale]/(public)/page.tsx b/apps/web/src/app/[locale]/(public)/page.tsx index af9bc9a..31f7b29 100644 --- a/apps/web/src/app/[locale]/(public)/page.tsx +++ b/apps/web/src/app/[locale]/(public)/page.tsx @@ -157,6 +157,9 @@ export default async function LandingPage({ params }: LocaleParams) { return (
+ {/* The stylesheet is inlined, so the browser could paint before the hero window has arrived and then grow + it. Hold the first paint until the hero has been parsed (browsers without rel=expect ignore this). */} +