perf: stop loading the app's stores in every plugin frame

This commit is contained in:
Linus Rath
2026-10-25 02:09:37 +02:00
parent 23eb3c5a2f
commit 782d4f8bf5
7 changed files with 47 additions and 9 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/cn";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/cn";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -0,0 +1,31 @@
// @vitest-environment node
import { describe, it, expect } from 'vitest';
import path from 'node:path';
import { build } from 'esbuild';
// Every plugin iframe parses and runs all the code the sandbox page imports,
// once per frame: sandboxed frames have opaque origins, so the browser's code
// cache never helps. The app's stores came in through the UI kit's Button
// (lib/utils -> settings store -> ten more stores), which made each frame
// boot ~100 ms slower.
describe('plugin sandbox runtime', () => {
it('does not pull the app stores into plugin frames', async () => {
const root = path.resolve(__dirname, '../..');
const result = await build({
entryPoints: [path.join(root, 'lib/plugin-sandbox/runtime.tsx')],
bundle: true,
write: false,
metafile: true,
format: 'esm',
platform: 'browser',
jsx: 'automatic',
alias: { '@': root },
external: ['@tabler/icons-react'],
loader: { '.css': 'empty' },
logLevel: 'silent',
});
const inputs = Object.keys(result.metafile.inputs).map((f) => f.replace(/\\/g, '/'));
expect(inputs.filter((f) => /(^|\/)stores\//.test(f))).toEqual([]);
expect(inputs.filter((f) => /node_modules\/zustand\//.test(f))).toEqual([]);
}, 30_000);
});
+3 -1
View File
@@ -221,7 +221,9 @@ describe('plugin dialog footer', () => {
await act(async () => {});
expect(result).toBeNull();
vi.doUnmock('@/components/plugins/plugin-iframe-slot');
});
// The dialog host is the first import here that loads the app's stores
// (the kit no longer does), and transforming them takes a while.
}, 30_000);
});
describe('Combobox', () => {
+9
View File
@@ -0,0 +1,9 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
// Kept apart from lib/utils, which imports the settings store: the plugin
// sandbox uses the app's Button and Input, and through lib/utils every
// sandbox frame loaded and ran ten app stores it never uses.
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+1 -1
View File
@@ -15,7 +15,7 @@
// against.
import * as React from 'react';
import { cn } from '@/lib/utils';
import { cn } from '@/lib/cn';
import { Button as AppButton } from '@/components/ui/button';
import { Input as AppInput } from '@/components/ui/input';
import { getFileIconByName, getFolderIcon } from '@/components/files/file-icons';
+1 -5
View File
@@ -1,5 +1,3 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { Mailbox } from "./jmap/types";
import { debug } from "./debug";
import { getEffectiveLocale } from '@/i18n/detect-locale';
@@ -7,9 +5,7 @@ import { useSettingsStore } from "@/stores/settings-store";
import type { DateFormat, DateLocale } from "@/stores/settings-store";
import { getEffectiveTimeZone, getWallClock } from "./timezone";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export { cn } from "./cn";
/**
* Strip embedded Basic-auth credentials from a URL for display.