Skip to content

Commit 4298dde

Browse files
committed
perf(context): lazy initialization for ExtensionsContext and LSPContext
Optimize startup performance by deferring expensive initialization work in both context providers until it is actually needed. ExtensionsContext.tsx: - Move get_extensions_directory invoke into the existing requestIdleCallback deferred block instead of eagerly calling it in a separate onMount - Batch setExtensions/setEnabledExtensions updates with SolidJS batch() to prevent double renders during extension loading - Add initialized signal to track when deferred loading completes - Expose initialized accessor on ExtensionsContextValue interface LSPContext.tsx: - Extract diagnostics event listener setup from onMount into an idempotent lazyInit() function guarded by a listenersInitialized flag - Call lazyInit() from startServer() so listeners are registered on first file open (triggered via useLSPEditor hook) - Keep requestIdleCallback fallback in onMount so listeners eventually register even without file opens - Add initialized field to LSPState store - Expose lazyInit method on LSPContextValue interface - Normalize line endings from CRLF to LF
1 parent 345f42c commit 4298dde

File tree

2 files changed

+48
-27
lines changed

2 files changed

+48
-27
lines changed

src/context/ExtensionsContext.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ export interface ExtensionsContextValue {
244244
loading: Accessor<boolean>;
245245
error: Accessor<string | null>;
246246
extensionsDir: Accessor<string>;
247+
initialized: Accessor<boolean>;
247248

248249
// Update State
249250
outdatedExtensions: Accessor<Map<string, ExtensionUpdateInfo>>;
@@ -418,6 +419,7 @@ export function ExtensionsProvider(props: ParentProps) {
418419
const [loading, setLoading] = createSignal(false);
419420
const [error, setError] = createSignal<string | null>(null);
420421
const [extensionsDir, setExtensionsDir] = createSignal("");
422+
const [initialized, setInitialized] = createSignal(false);
421423

422424
// Update state using store for reactive nested updates
423425
const [updateState, setUpdateState] = createStore<ExtensionUpdateState>({
@@ -488,25 +490,17 @@ export function ExtensionsProvider(props: ParentProps) {
488490
stopWorkerHost();
489491
});
490492

491-
// Load extensions directory path on mount
492-
onMount(async () => {
493-
try {
494-
const dir = await invoke<string>("get_extensions_directory");
495-
setExtensionsDir(dir);
496-
} catch (e) {
497-
console.error("Failed to get extensions directory:", e);
498-
}
499-
});
500-
501493
// Load all extensions
502494
const loadExtensions = async () => {
503495
setLoading(true);
504496
setError(null);
505497
try {
506498
const loaded = await invoke<Extension[]>("load_extensions");
507-
setExtensions(loaded);
508-
const enabled = loaded.filter((ext) => ext.enabled);
509-
setEnabledExtensions(enabled);
499+
batch(() => {
500+
setExtensions(loaded);
501+
const enabled = loaded.filter((ext) => ext.enabled);
502+
setEnabledExtensions(enabled);
503+
});
510504
// Also refresh themes when extensions are loaded
511505
await refreshThemes();
512506
} catch (e) {
@@ -1502,7 +1496,16 @@ export function ExtensionsProvider(props: ParentProps) {
15021496
// Use requestIdleCallback to defer extension loading until browser is idle
15031497
// This prevents blocking the main thread during startup
15041498
const loadExtensionsDeferred = async () => {
1499+
// Load extensions directory path (deferred from mount for startup perf)
1500+
try {
1501+
const dir = await invoke<string>("get_extensions_directory");
1502+
setExtensionsDir(dir);
1503+
} catch (e) {
1504+
console.error("Failed to get extensions directory:", e);
1505+
}
1506+
15051507
await loadExtensions();
1508+
setInitialized(true);
15061509

15071510
// Setup event listener for update-related events
15081511
try {
@@ -1566,6 +1569,7 @@ export function ExtensionsProvider(props: ParentProps) {
15661569
loading,
15671570
error,
15681571
extensionsDir,
1572+
initialized,
15691573

15701574
// Update state
15711575
outdatedExtensions,

src/context/LSPContext.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -565,10 +565,13 @@ interface LSPState {
565565
languageStatus: Record<string, LanguageStatusItem[]>;
566566
loading: boolean;
567567
error: string | null;
568+
initialized: boolean;
568569
}
569570

570571
interface LSPContextValue {
571572
state: LSPState;
573+
// Lazy initialization - call when first file is opened
574+
lazyInit: () => void;
572575
// Server management
573576
startServer: (config: LanguageServerConfig) => Promise<ServerInfo>;
574577
stopServer: (serverId: string) => Promise<void>;
@@ -699,21 +702,19 @@ export function LSPProvider(props: ParentProps) {
699702
languageStatus: {},
700703
loading: false,
701704
error: null,
705+
initialized: false,
702706
});
703707

704708
let unlistenDiagnostics: UnlistenFn | undefined;
709+
let listenersInitialized = false;
705710

706-
// Register cleanup synchronously
707-
onCleanup(() => {
708-
unlistenDiagnostics?.();
709-
// Stop all servers on cleanup
710-
stopAllServers().catch(console.error);
711-
});
711+
// Lazy initialization - sets up event listeners on first file open
712+
// Idempotent: safe to call multiple times
713+
const lazyInit = () => {
714+
if (listenersInitialized) return;
715+
listenersInitialized = true;
712716

713-
onMount(() => {
714-
// DEFERRED - Set up LSP diagnostics listener after first paint
715-
// LSP diagnostics won't fire until a language server is started and files are opened
716-
const initDeferredListeners = async () => {
717+
const initListeners = async () => {
717718
// Listen for diagnostics events from the backend
718719
unlistenDiagnostics = await listen<{
719720
server_id: string;
@@ -731,7 +732,7 @@ export function LSPProvider(props: ParentProps) {
731732
}>;
732733
}>("lsp:diagnostics", (event) => {
733734
const { uri, diagnostics } = event.payload;
734-
735+
735736
const mapped: Diagnostic[] = diagnostics.map((d) => ({
736737
range: d.range,
737738
severity: mapSeverity(d.severity),
@@ -746,13 +747,26 @@ export function LSPProvider(props: ParentProps) {
746747

747748
setState("diagnostics", uri, { uri, diagnostics: mapped });
748749
});
750+
751+
setState("initialized", true);
749752
};
750753

751-
// Use requestIdleCallback if available, otherwise setTimeout
754+
initListeners().catch(console.error);
755+
};
756+
757+
// Register cleanup synchronously
758+
onCleanup(() => {
759+
unlistenDiagnostics?.();
760+
// Stop all servers on cleanup
761+
stopAllServers().catch(console.error);
762+
});
763+
764+
// Deferred fallback: set up listeners during idle time even if no file is opened
765+
onMount(() => {
752766
if ('requestIdleCallback' in window) {
753-
(window as Window & { requestIdleCallback: (cb: () => void, opts?: { timeout: number }) => number }).requestIdleCallback(initDeferredListeners, { timeout: 2000 });
767+
(window as Window & { requestIdleCallback: (cb: () => void, opts?: { timeout: number }) => number }).requestIdleCallback(() => lazyInit(), { timeout: 2000 });
754768
} else {
755-
setTimeout(initDeferredListeners, 100);
769+
setTimeout(() => lazyInit(), 100);
756770
}
757771
});
758772

@@ -767,6 +781,8 @@ export function LSPProvider(props: ParentProps) {
767781
};
768782

769783
const startServer = async (config: LanguageServerConfig): Promise<ServerInfo> => {
784+
// Ensure event listeners are set up before starting any server
785+
lazyInit();
770786
setState("loading", true);
771787
setState("error", null);
772788

@@ -1829,6 +1845,7 @@ export function LSPProvider(props: ParentProps) {
18291845
<LSPContext.Provider
18301846
value={{
18311847
state,
1848+
lazyInit,
18321849
startServer,
18331850
stopServer,
18341851
stopAllServers,

0 commit comments

Comments
 (0)