Skip to content

perf(dashboard): Drop zod locales and Drizzle from the browser bundle - #2731

Merged
BYK merged 4 commits into
mainfrom
perf/dashboard-bundle-split
Nov 5, 2026
Merged

BYK merged 4 commits into
mainfrom
perf/dashboard-bundle-split

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

The dashboard entry (client-[hash].js) is 2.85 MB raw. An esbuild metafile showed about 400 KB of it comes from code the browser never runs.

entry raw gzip-9 br-11
main 2,847,273 814,316 661,472
this PR 2,443,134 739,683 605,140
change −404 KB (−14%) −75 KB −56 KB

The saving is bigger raw than compressed because locale tables compress well. The browser still has to parse and compile less JS, which matters most on phones.

Changes

  • zod locales: 426 KB → 103 KB. import { z } from "zod" reads the export * as z namespace object. esbuild can't tree-shake that, so every zod locale (he, ru, ta, …, 248 KB) and the JSON-schema code stayed in the bundle. import * as z from "zod" gives the same API (zod 4 docs use it) and shakes correctly. On a one-line test bundle, the named import gives 433 KB and the namespace import 81 KB. This is a repo-wide codemod (325 files, import lines only), because one leftover named import anywhere in the browser graph brings everything back.
    • New oxlint rule junior/zod-namespace-import (in tools/oxlint/junior) keeps it that way. It is on in the junior config, which the plugin packages and junior-plugin-api share, and in the dashboard config. Root pnpm lint now also lints junior-plugin-api, because its code ships in the bundle. Type-only imports are allowed. The built-in no-restricted-imports can't do this: with importNames: ["z"] it also rejects import * as z.
  • drizzle-orm: 34 KB → 0. @sentry/junior/api/schema imported AGENT_INVOCATION_STATUSES and juniorDestinationKindSchema from the Drizzle table modules. Those modules call pgTable(...) at the top level, so the tables and drizzle-orm came along. The values now live in chat/agent-invocations/statuses.ts and db/schema/destination-kind.ts, and the table modules import them. db:generate reports no schema changes.
  • One copy of the plugin API: −17 KB. With injectWorkspacePackages, @sentry/junior has its own injected copy of @sentry/junior-plugin-api, so the browser loaded both. The client build now aliases the package to one resolved path.

Not in this PR

Splitting routes comes next, as a separate PR with screenshots. The entry still eagerly includes recharts plus d3 and redux (~348 KB, used only by charts), every page under src/client/pages (225 KB), and the emoji shortcode table (47 KB).

Checks

  • pnpm typecheck, pnpm lint (including the new rule test in oxlint-rules:check), and db:generate (no changes).
  • Dashboard e2e: 89/89 passed, run locally with the CI env unset.
  • Dashboard unit tests: 368/369. The one failure is dashboard-output.test.ts (nitro build: nitroIntegration is not exported by a stale @sentry/server-utils@11.6.0 in this sandbox). It's environmental and not caused by this diff; perf(dashboard): Keep the detail version when the reader marks a Conversation read #2720 reported the same failure on main.
  • Integration tests: child-agents, locations/*, conversation detail, and list-usage (15/15).

No UI change, so no screenshots.

--

View Session in Sentry

Import zod as a namespace everywhere. esbuild cannot tree-shake the named
z export, so it kept all zod locales (~250 KB) in the dashboard entry.
An ast-grep rule keeps the named import out.

Move the agent invocation statuses and the destination kind schema out of
the Drizzle table modules, so @sentry/junior/api/schema no longer pulls
drizzle-orm into the browser. Resolve @sentry/junior-plugin-api to one
copy in the client build, so the browser loads it once.
@vercel

vercel Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Nov 5, 2026 10:40am UTC

Request Review

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

2 screenshot changes — 2 changed · 0 added · 0 removed

Review screenshots in Frameshift

Automation Page · Desktop
Automation Page · Desktop
Changed
Settings Mcp · Desktop
Settings Mcp · Desktop
Changed

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always prefer oxlint rules over ast-grep when possible and makes sense.

@BYK
BYK merged commit 53eb1a8 into main Nov 5, 2026
51 checks passed
@BYK
BYK deleted the perf/dashboard-bundle-split branch November 5, 2026 10:56
BYK pushed a commit that referenced this pull request Nov 5, 2026
…and (#2734)

Follow-up to #2731. The browser downloaded every dashboard page,
recharts with its d3/redux deps, and Shiki's core before it could paint
the conversation workspace. Now each of those loads when it's first
needed.

First load = the entry plus every chunk it imports statically (esbuild
metafile of `tsup.client.config.ts`):

| first load | files | raw | gzip-9 | br-11 |
|---|---|---|---|---|
| main | 2 | 2,448,020 | 741,377 | 606,927 |
| this PR | 55 | 1,712,998 | 550,926 | 478,718 |
| change | | −735 KB (−30%) | −190 KB (−26%) | −128 KB (−21%) |

Recharts, d3, redux, immer, the Shiki core and TextMate engine, and all
`src/client/pages` code except the layouts and loading skeletons are no
longer on the first load.

## Changes
- **Lazy pages:** in `App.tsx`, every route page outside the
conversation workspace is now `React.lazy`: automations, code,
locations, people, settings, system, workspaces, plugin and memory
pages, and admin. The conversation workspace and review desk stay eager.
Layouts and loading skeletons stay eager, so route-loading states still
show at once. `TracedRoutes` takes a `fallback` and wraps the matched
route in `Suspense`.
- **`pluginUserPagePath` moved** to
`pages/user/pluginUserPageRoutes.ts`. `ProfileMenu` and `primaryNav`
imported the helper from `PluginUserPage.tsx`, and that pulled the
memory page and recharts into the entry. This uses the same pattern as
`codeRepositoryRoutes.ts`.
- **Shiki loads with the first code block.** The highlighter moved to
`code-highlighter.ts`, and `HighlightedCode` imports it inside its
existing async query. Before the first highlight it shows the same plain
`HighlightedCodeFallback` that it already showed.

esbuild lifts every shared chunk into a direct import of the entry, so
all 55 first-load files are requested together right after the entry.
Asset requests are counted in the QA below.

## Not in this PR
- `modulepreload` hints in the HTML shell. They would let those chunks
download in parallel with the entry, but the shell would need a build
manifest of the entry's imports.
- `emojibase` shortcodes (47 KB). The conversation list needs them to
render text on first paint.
- `@base-ui/react` (208 KB) is already imported per component and
tree-shaken.

## QA
Mock dashboard (`JUNIOR_DASHBOARD_MOCK_CONVERSATIONS=true pnpm dev`) in
Playwright:
- Home loads 55 asset files, none of them page or chart code.
- Moving from home to System inside the app fetches 6 more chunks.
System → People fetches 5 more.
- SQL in the conversation still highlights.
- No page errors and no 4xx responses.

Automations and Settings redirect to home with dashboard auth off, the
same as main, so they're covered by e2e instead.

| Home | Conversation (code highlighted) |
|---|---|
|
![home](https://junior-prod.sentry.dev/public/artifacts/b7ee4eb5-bd18-4381-af33-04abf91a2b83.png)
|
![conversation](https://junior-prod.sentry.dev/public/artifacts/6885c690-872c-4522-b0b8-eb0f2923c107.png)
|

| System (in-app navigation, charts from a lazy chunk) | People |
|---|---|
|
![system](https://junior-prod.sentry.dev/public/artifacts/962cd0c8-23da-4cb4-b81d-7d0884545cbb.png)
|
![people](https://junior-prod.sentry.dev/public/artifacts/c3759d72-b4e4-430e-8c67-0a09161aa9b9.png)
|

<img alt="home on mobile"
src="https://junior-prod.sentry.dev/public/artifacts/2c2f7024-e8da-4534-898e-0121ad0e8b5d.png"
width="300">

## Checks
- Dashboard `typecheck` and `lint` pass, and so do `file-length:check`
and `dashboard-style:check`.
- Dashboard e2e: 95/95 passed, run locally with the CI env unset.
- Dashboard unit tests: 368/369. The one failure is
`dashboard-output.test.ts`, caused by the stale
`@sentry/server-utils@11.6.0` (`nitroIntegration` missing) in this
sandbox. It fails the same way on main.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A17b6d7f555623680001f2a90 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A17b6d7f555623680001f2a90/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview – junior-docs — aa13350c Deployed Nov 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant