Skip to content

Env vars page performance fix#3829

Draft
kathiekiwi wants to merge 3 commits into
mainfrom
env-vars-page-performance-fix
Draft

Env vars page performance fix#3829
kathiekiwi wants to merge 3 commits into
mainfrom
env-vars-page-performance-fix

Conversation

@kathiekiwi
Copy link
Copy Markdown
Collaborator

Summary

This PR improves performance across the Environment Variables page.

Changes

Targeted value loading

  • load only the non-secret (environmentId, key) pairs required by the page. Secret values continue to be redacted in the UI.

SSR windowing + virtualization

  • SSR-render only the first 50 rows
  • hydrate those rows
  • virtualize the remaining dataset client-side
  • search is now URL-driven during SSR, ensuring deep links such as ?search=DATABASE_URL

Lightweight 'Create' flow

  • 'Create' page no longer loads the full Environment Variables dataset.

Results

Large projects no longer render thousands of rows during SSR.
Example (~11k rendered rows):

Metric Before After
Document size ~150 MB ~5 MB
SSR rows ~11k 50
Browser DOM rows Thousands ~26–38

Testing

Automated

  • pnpm run test ./test/environmentVariablesRepository.test.ts
  • pnpm run test ./test/environmentVariablesEnvironments.test.ts
  • pnpm run test ./test/EnvironmentVariablesPresenter.test.ts

Manual

  • Environment Variables list loads without browser freezes
  • Search works via URL parameters
  • Secret values remain redacted
  • Non-secret values render correctly
  • Create page loads successfully
  • Create/edit/delete flows continue to work
  • Virtualized table preserves column alignment and sticky headers

Ekaterina Bulatova added 2 commits June 4, 2026 00:23
The Environment Variables page presenter loaded the entire project
secret store via a prefix scan and decrypted every value on each
render — including secret values that are immediately masked in the
UI — then matched rows with nested O(N×M²) `.find()` lookups.

- Collect only the non-secret (environmentId, key) pairs and fetch
  them with a targeted `key IN (...)` query; decrypt only those.
- Add `getSecretsByKeys` to the secret store and
  `getVariableValuesForKeys` to the repository for this access path.
- Replace the nested `.find()` lookups with O(1) Map lookups keyed
  by `${environmentId}:${key}`.

Cuts per-render decryption and server CPU for projects with many
variables and environments; secret values stay masked as before.
The page server-rendered every row (~13 KB of markup each), producing
a tens-of-MB HTML document and mounting thousands of row components on
hydration, which froze the browser for projects with many variables
across many environments.

- Server-render only the first 50 rows, hydrate those, then switch to
  @tanstack/react-virtual over the full dataset after mount via
  useLayoutEffect (server and first client render match — no
  hydration mismatch).
- Virtualize with a spacer-row technique inside the existing <table>
  so column widths and the sticky header are preserved; extract a
  shared EnvironmentVariableTableRow used by both the SSR and virtual
  paths to avoid drift.
- Seed useFuzzyFilter from the URL `search` param (controlled mode,
  matching the Tasks page) so filtering happens at SSR and deep links
  render the correct rows in the initial window.

For ~11k rows the document drops from ~150 MB to ~5 MB with 50 SSR
rows; the load freeze is gone.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 3, 2026

⚠️ No Changeset found

Latest commit: 8256c34

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c55e2ec3-c74b-41ea-ab70-0de3cf0f5657

📥 Commits

Reviewing files that changed from the base of the PR and between 3142404 and 8256c34.

📒 Files selected for processing (7)
  • .server-changes/environment-variables-page-performance.md
  • apps/webapp/app/presenters/v3/CreateEnvironmentVariablesPresenter.server.ts
  • apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts
  • apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx
  • apps/webapp/test/environmentVariablesEnvironments.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .server-changes/environment-variables-page-performance.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/webapp/app/presenters/v3/CreateEnvironmentVariablesPresenter.server.ts
  • apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx
  • apps/webapp/test/environmentVariablesEnvironments.test.ts
  • apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: Analyze (javascript-typescript)

Walkthrough

This PR implements backend and frontend performance optimizations for the Environment Variables page. The changes add a bulk-secret retrieval API to the SecretStore, extend the EnvironmentVariablesRepository with a method to fetch variable values for specified keys only, introduce a shared loadEnvironmentVariablesEnvironments utility to reduce duplication, and create a lightweight CreateEnvironmentVariablesPresenter for the create-new-variable route. The main EnvironmentVariablesPresenter is refactored to use these new utilities. The frontend route now returns discriminated loader data and branches on isCreateRoute to either show a create layout or list UI. Row virtualization using @tanstack/react-virtual is added with a static prefix plus virtualized remainder, and table rows are extracted into reusable components. Comprehensive test fixtures and suites validate the loaders, presenters, and repository methods. Documentation describes the approach.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Env vars page performance fix' directly describes the main objective of the changeset: improving performance for the Environment Variables page.
Description check ✅ Passed The PR description includes a clear summary, detailed changes section, concrete results with metrics, comprehensive testing coverage (both automated and manual), but lacks the issue reference and checklist items from the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch env-vars-page-performance-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/webapp/scripts/measure-environment-variables-html.mts Fixed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
apps/webapp/app/services/secrets/secretStore.server.ts (1)

144-174: ⚡ Quick win

Batch parsing aborts entirely on a single schema-validation failure.

Invalid JSON is handled gracefully (log + undefined), but schema.parse at Line 149 and Line 173 throws. Because #parseStoredSecrets calls this per secret, one malformed/legacy entry now causes getSecretsByKeys/getSecrets to reject for the whole batch — which can break the entire Environment Variables SSR page rather than dropping one row. Consider making the batch path tolerant (skip + log) while keeping the single-secret getSecret path strict.

♻️ One option: tolerant batch wrapper
   for (const secret of secrets) {
-    const value = await this.#parseStoredSecret(schema, secret);
-    if (value !== undefined) {
-      results.push({ key: secret.key, value });
-    }
+    try {
+      const value = await this.#parseStoredSecret(schema, secret);
+      if (value !== undefined) {
+        results.push({ key: secret.key, value });
+      }
+    } catch (error) {
+      logger.error(`Failed to parse secret ${secret.key}`, { error });
+    }
   }
Please confirm whether a single corrupt secret should fail the whole page load; if strict behavior is intended, ignore this.
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx (1)

701-705: ⚡ Quick win

Add key props to spacer rows.

The spacer <tr> elements should have explicit key props to prevent React warnings during re-renders.

🔧 Suggested fix
       {topSpacerHeight > 0 && (
-        <tr aria-hidden style={{ height: topSpacerHeight }}>
+        <tr key="top-spacer" aria-hidden style={{ height: topSpacerHeight }}>
           <td colSpan={columnCount} />
         </tr>
       )}
       {virtualItems.map((virtualRow) => {
         const variable = groupedEnvironmentVariables[virtualRow.index];
         if (!variable) {
           return null;
         }

         return (
           <EnvironmentVariableTableRow
             key={`${variable.id}-${variable.environment.id}`}
             variable={variable}
             revealAll={revealAll}
             vercelIntegration={vercelIntegration}
           />
         );
       })}
       {bottomSpacerHeight > 0 && (
-        <tr aria-hidden style={{ height: bottomSpacerHeight }}>
+        <tr key="bottom-spacer" aria-hidden style={{ height: bottomSpacerHeight }}>
           <td colSpan={columnCount} />
         </tr>
       )}

Also applies to: 721-725

apps/webapp/test/environmentVariablesRepository.test.ts (1)

129-136: 💤 Low value

Reuse the uniqueId helper instead of raw Date.now() for slug/externalRef.

This inline creation duplicates the fixture's project-creation logic and relies on Date.now() alone for uniqueness, which is weaker than the uniqueId helper (counter + timestamp) used elsewhere. Prefer the shared helper for consistency and collision resistance.

♻️ Suggested change
-      data: {
-        name: "Project B",
-        slug: `proj-b-${Date.now()}`,
-        organizationId: organization.id,
-        externalRef: `ext-b-${Date.now()}`,
-      },
+      data: {
+        name: "Project B",
+        slug: uniqueId("proj-b"),
+        organizationId: organization.id,
+        externalRef: uniqueId("ext-b"),
+      },

Add uniqueId to the fixtures import on line 24-28.

apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts (1)

123-131: ⚡ Quick win

Residual nested .find lookups undercut the stated O(1) goal.

nonSecretItems (here) and the final flatMap (Line 154) both iterate environmentVariables × sortedEnvironments and call .find over values on each pair. The PR aims to replace nested .find() with O(1) Map lookups; consider building a Map<environmentId, valueRecord> per environmentVariable once and reusing it in both passes to keep the hot path linear.

♻️ Sketch
-    const nonSecretItems: Array<{ environmentId: string; key: string }> = [];
-    for (const environmentVariable of environmentVariables) {
-      for (const env of sortedEnvironments) {
-        const valueRecord = environmentVariable.values.find((v) => v.environmentId === env.id);
-        if (valueRecord && !valueRecord.isSecret) {
-          nonSecretItems.push({ environmentId: env.id, key: environmentVariable.key });
-        }
-      }
-    }
+    const valuesByEnvForVariable = new Map(
+      environmentVariables.map((ev) => [
+        ev.id,
+        new Map(ev.values.map((v) => [v.environmentId, v])),
+      ])
+    );
+    const nonSecretItems: Array<{ environmentId: string; key: string }> = [];
+    for (const environmentVariable of environmentVariables) {
+      const byEnv = valuesByEnvForVariable.get(environmentVariable.id)!;
+      for (const env of sortedEnvironments) {
+        const valueRecord = byEnv.get(env.id);
+        if (valueRecord && !valueRecord.isSecret) {
+          nonSecretItems.push({ environmentId: env.id, key: environmentVariable.key });
+        }
+      }
+    }

Then reuse valuesByEnvForVariable in the final flatMap instead of .find.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 92f33967-95f9-4d5b-8b90-7f0d6902b836

📥 Commits

Reviewing files that changed from the base of the PR and between d1f4302 and 74b25d6.

📒 Files selected for processing (18)
  • .server-changes/environment-variables-page-performance.md
  • apps/webapp/app/components/primitives/Table.tsx
  • apps/webapp/app/presenters/v3/CreateEnvironmentVariablesPresenter.server.ts
  • apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts
  • apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx
  • apps/webapp/app/services/secrets/secretStore.server.ts
  • apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts
  • apps/webapp/app/v3/environmentVariables/repository.ts
  • apps/webapp/scripts/measure-create-environment-variables-loader.mts
  • apps/webapp/scripts/measure-environment-variables-html.mts
  • apps/webapp/scripts/measure-environment-variables-new-parent-loader.mts
  • apps/webapp/scripts/spike-environment-variables-table-dom.mts
  • apps/webapp/test/EnvironmentVariablesPresenter.test.ts
  • apps/webapp/test/environmentVariablesEnvironments.test.ts
  • apps/webapp/test/environmentVariablesRepository.test.ts
  • apps/webapp/test/fixtures/environmentVariablesFixtures.ts

@kathiekiwi kathiekiwi force-pushed the env-vars-page-performance-fix branch from 74b25d6 to 3142404 Compare June 3, 2026 22:52
Comment thread apps/webapp/scripts/measure-environment-variables-html.mts Fixed
Opening /environment-variables/new ran the full list presenter twice —
once in the parent route loader and once in the child loader — fetching
and decrypting every variable value just to show the create form, which
only needs the list of environments.

- Short-circuit the parent route loader on the /new path so it skips
  the list presenter entirely and renders only the create outlet.
- Load just the environment list in the child route via a new
  CreateEnvironmentVariablesPresenter.
- Extract the shared environment-loading logic into
  loadEnvironmentVariablesEnvironments, preserving the project access
  check and environment filtering for both presenters.

Removes the heavy presenter work (full fetch + decrypt) when opening
the create form.
@kathiekiwi kathiekiwi force-pushed the env-vars-page-performance-fix branch from 3142404 to 8256c34 Compare June 4, 2026 09:25
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.

2 participants