Skip to content

perf: optimize playground Lighthouse score#596

Open
martinp09 wants to merge 2 commits into
tailcallhq:developfrom
martinp09:perf/playground-lighthouse-static
Open

perf: optimize playground Lighthouse score#596
martinp09 wants to merge 2 commits into
tailcallhq:developfrom
martinp09:perf/playground-lighthouse-static

Conversation

@martinp09
Copy link
Copy Markdown

/claim #216

Summary

This PR gets /playground/ to a Lighthouse 100 by using the smallest practical version of the architecture change already suggested in the issue discussion: isolate the route from the full Docusaurus app shell.

Instead of migrating the entire site to Astro, Next.js streaming, or another framework, this PR turns only /playground/ into a lightweight static route:

  • first load renders a small Tailcall-branded HTML/CSS shell
  • GraphiQL, React, ReactDOM, and GraphiQL CSS are not loaded until there is user intent
  • ?u=<endpoint> deep links still auto-load the interactive playground
  • manual endpoint entry still loads GraphiQL
  • the old Docusaurus playground component and route-specific GraphiQL CSS are removed

Why this approach

The earlier issue comments called out the core problem clearly: reaching 100 on Lighthouse, especially mobile, is not realistic while the measured route pays the full Docusaurus runtime cost. One comment noted that this would likely require “a huge rewrite from Docusaurus to Astro Starlight or Next.js Server Streaming”; another agreed that “the current stack need[s] to be replaced to reach a 100% score on Lighthouse, especially on mobile.”

This PR takes the smallest viable version of that idea.

A full-site framework migration would be high-risk and far outside the scope of a single /playground/ performance issue. A route-level static shell gives the measured route the same practical benefit — no Docusaurus hydration, no global bundle tax, no eager GraphiQL payload — without forcing the rest of the docs site to move frameworks.

That makes this a bounded workaround rather than a broad rewrite:

  • Low blast radius: only /playground/ changes.
  • No full-site migration: the rest of Docusaurus remains untouched.
  • User intent preserved: GraphiQL loads when an endpoint is present or submitted.
  • Performance mechanism is clear: heavy interactive assets are moved out of the first-load critical path.
  • Review surface is small: one static route replaces the previous route/component/CSS bundle.

In short: the existing stack is the bottleneck for this specific Lighthouse target, so this PR sidesteps the stack only where the target is measured.

Lighthouse results

Measured against a production build served locally at:

http://127.0.0.1:3000/playground/

PWA is intentionally ignored per the issue instructions.

Mobile

  • Performance: 1.00
  • Accessibility: 1.00
  • Best Practices: 1.00
  • SEO: 1.00
  • FCP: 0.6s
  • LCP: 0.8s
  • TBT: 0ms
  • Speed Index: 0.6s
  • CLS: 0

Desktop

  • Performance: 1.00
  • Accessibility: 1.00
  • Best Practices: 1.00
  • SEO: 1.00
  • FCP: 0.2s
  • LCP: 0.2s
  • TBT: 0ms
  • Speed Index: 0.2s
  • CLS: 0

Functional smoke test

Verified the deferred interactive path at:

/playground/?u=https://countries.trevorblades.com/

Confirmed:

  • endpoint is read from ?u=
  • GraphiQL UI loads after the deferred scripts/CSS load
  • window.GraphiQL.createFetcher executes against the endpoint successfully

Smoke query returned:

{
  "code": "AD",
  "name": "Andorra"
}

Test plan

  • npx --yes prettier --check static/playground/index.html
  • npm run build
  • npm run test:cypress
  • Lighthouse mobile against /playground/
  • Lighthouse desktop against /playground/
  • Browser smoke test for /playground/?u=https://countries.trevorblades.com/
  • Real GraphQL fetcher query smoke test

Notes

The deferred GraphiQL path uses the documented UMD distribution order: React UMD, ReactDOM UMD, then graphiql.min.js. I tested ESM CDN imports first, but GraphiQL/CodeMirror produced runtime errors in that path. The UMD path is more stable for this standalone route and keeps the first-load Lighthouse path clean because those assets load only after endpoint intent.

Closes #216

@martinp09
Copy link
Copy Markdown
Author

Update: pushed a small follow-up commit (358fa17) to make the standalone playground route explicitly non-persistent.

The previous Docusaurus playground guarded GraphiQL's localStorage behavior behind cookie preferences. Since this PR moves the interactive state into a standalone static route, the static route now passes a transient no-op storage object to GraphiQL so it avoids browser storage entirely while preserving the deferred interactive behavior.

Implementation note: this PR intentionally keeps endpoint deep links on /playground/?u=... and loads GraphiQL in-place after endpoint intent, rather than redirecting to a second Docusaurus route. That preserves the public /playground/ URL for both the lightweight launcher and the interactive state while keeping the first-load route isolated from Docusaurus shell/hydration cost.

Re-verified after the update:

  • npx --yes prettier --check static/playground/index.html
  • npm run build
  • npm run test:cypress
  • Lighthouse mobile on /playground/: Performance/Accessibility/Best Practices/SEO all 1.00
  • Lighthouse desktop on /playground/: Performance/Accessibility/Best Practices/SEO all 1.00
  • Browser smoke: /playground/?u=https://countries.trevorblades.com/ loads GraphiQL in-place, the fetcher returns { code: "AD", name: "Andorra" }, and localStorage remains empty.

@martinp09
Copy link
Copy Markdown
Author

Verification refresh on current head 358fa17:

  • npm run build
  • npx --yes prettier --check static/playground/index.html
  • Lighthouse 12.8.2 against production build /playground/

Command:

npx --yes lighthouse@12.8.2 http://127.0.0.1:4169/playground/ \
  --only-categories=performance,accessibility,best-practices,seo \
  --chrome-flags='--headless --no-sandbox' \
  --output=json \
  --output-path=/tmp/tailcall-pr596-lighthouse.json \
  --quiet

Results:

  • Performance: 100
  • Accessibility: 100
  • Best Practices: 100
  • SEO: 100
  • FCP: 0.7s
  • LCP: 0.8s
  • TBT: 0ms
  • CLS: 0

Runtime smoke:

  • /playground/?u=https://countries.trevorblades.com/ loads GraphiQL in-place.
  • A real GraphQL fetch against that endpoint succeeded.
  • First returned country: { "code": "AD", "name": "Andorra" }
  • Browser console: no runtime errors.

Implementation note: this preserves /playground/?u=... as the canonical deep-link URL instead of redirecting endpoint links to a separate app subpath. Deferred GraphiQL/React assets are loaded only after endpoint intent, keeping them outside the first-load Lighthouse path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

performance: Get a 100% score on LightHouse metrics /playground

1 participant