Skip to content

Commit 3a6a346

Browse files
committed
improvement(perf): eight verified cuts to workspace cold-load JavaScript
Second round of load-time work, adversarially verified for strict behaviour preservation before implementation. Each item is an import-graph fix — none changes what renders, when it renders, or any data path: - knowledge/[id] imported one modal through the [documentId] components barrel, which also exports the chunk editor and therefore js-tiktoken (~2.5 MB gzip of BPE tables) on a route that never edits chunks. Deep import. - prepareBlockState moved out of stores/workflows/utils.ts into its own module. It is the only function there needing the block registry and the generated tool-outputs artifact (~476 KB gzip), and utils.ts is reached by the persistent shell — so every workspace route paid for a canvas-only helper, including a module-scope JSON.parse of a 5.4 MB string. - ExecutionSnapshot (the frozen-canvas modal) is now React.lazy behind its interaction gates, per the code-splitting procedure in sim-imports.md: deep import, dead barrel re-export deleted, sibling imports in log-details deepened to break the parent->child barrel cycle, local Suspense at both render sites. Takes ~7.6 MB of source off logs hydration. - The api contracts barrel no longer re-exports ./tools, ./selectors, ./v1, or ./demo-requests (~58 KB gzip of Zod schema construction on every route). Zero importers used the barrel path for any of them. - createCsvParser (streaming csv-parse, a Node Transform) moved to a server-only module so its stream polyfill leaves client bundles. Deliberately not re-exported from the lib/table barrel. - jszip is dynamically imported at both remaining static call sites (skill zip extraction, pptx parsing) — both already-async, user-triggered paths, mirroring the existing pattern in workflow import-export. - The desktop local-filesystem tool executor is dynamically imported in use-chat; a chunk-load failure now reports an error completion so the server-side tool call settles instead of hanging. Production build, JS downloaded before the load event, vs the previous release: /home 4.44 -> 3.87 MB /logs 4.44 -> 3.64 MB /knowledge 4.22 -> 3.68 MB /tables 4.17 -> 3.61 MB /files 4.68 -> 4.10 MB /w/[id] 4.80 -> 4.67 MB /home total after idle prefetch: 8.15 -> 5.52 MB The lazy snapshot was exercised end-to-end: its chunk loads when a log detail opens (off the route's cold path, warm before the View Snapshot click) and the modal renders without errors. Boundary baseline retightened.
1 parent b654bc1 commit 3a6a346

19 files changed

Lines changed: 481 additions & 372 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ import {
7474
TERMINAL_SESSION_RESOURCE_ID,
7575
} from '@/lib/copilot/resources/types'
7676
import { executeBrowserToolOnClient } from '@/lib/copilot/tools/client/browser-tool-execution'
77-
import { executeLocalFilesystemTool } from '@/lib/copilot/tools/client/local-filesystem'
7877
import {
7978
bindRunToolToExecution,
8079
cancelRunToolExecution,
@@ -2009,11 +2008,36 @@ export function useChat(
20092008
return
20102009
}
20112010
handledClientLocalFilesystemToolIdsRef.current.add(toolCallId)
2012-
executeLocalFilesystemTool(toolCallId, toolName, toolArgs, {
2011+
const options = {
20132012
workspaceId,
20142013
chatId: chatIdRef.current ?? selectedChatIdRef.current,
20152014
signal: abortControllerRef.current?.signal,
2016-
})
2015+
}
2016+
/**
2017+
* Dynamic on purpose: the local-filesystem executor only runs for desktop-local
2018+
* VFS tool calls, and a static import kept it in the shared chat chunk on every
2019+
* surface that mounts the composer. The guard, the dedupe add, and the option
2020+
* capture above stay synchronous, so re-entrancy behaviour is unchanged. If the
2021+
* chunk fails to load (deploy skew), the server-side tool call must still settle:
2022+
* report an error completion rather than leaving it hanging with the dedupe ref
2023+
* already marked handled.
2024+
*/
2025+
import('@/lib/copilot/tools/client/local-filesystem').then(
2026+
(m) => m.executeLocalFilesystemTool(toolCallId, toolName, toolArgs, options),
2027+
async (error) => {
2028+
logger.error('Failed to load local filesystem tool executor', { error })
2029+
const [{ reportClientToolCompletion }, { ASYNC_TOOL_CONFIRMATION_STATUS }] =
2030+
await Promise.all([
2031+
import('@/lib/copilot/tools/client/completion'),
2032+
import('@/lib/copilot/async-runs/lifecycle'),
2033+
])
2034+
await reportClientToolCompletion(
2035+
toolCallId,
2036+
ASYNC_TOOL_CONFIRMATION_STATUS.error,
2037+
'Local filesystem tool failed to load'
2038+
)
2039+
}
2040+
)
20172041
},
20182042
[workspaceId]
20192043
)

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ import {
7777
useFolderAncestors,
7878
} from '@/app/workspace/[workspaceId]/components/folders'
7979
import { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
80-
import { DocumentTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components'
80+
/**
81+
* Deep import on purpose: the `[documentId]/components` barrel also exports `ChunkEditor`,
82+
* which needs exact token counts and therefore `js-tiktoken` (~2.5 MB gzip of BPE rank
83+
* tables). Importing the modal through the barrel shipped the tokenizer to the document
84+
* LIST route, which never edits chunks.
85+
*/
86+
import { DocumentTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal'
8187
import {
8288
ActionBar,
8389
AddConnectorModal,
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export { Dashboard } from './dashboard'
22
export { LogDetails, LogDetailsContent } from './log-details'
3-
export { ExecutionSnapshot } from './log-details/components/execution-snapshot'
43
export { FileCards } from './log-details/components/file-download'
54
export { TraceView } from './log-details/components/trace-view'
65
export { LogRowContextMenu } from './log-row-context-menu'

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
'use client'
22

3-
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
3+
import {
4+
lazy,
5+
memo,
6+
Suspense,
7+
useCallback,
8+
useEffect,
9+
useLayoutEffect,
10+
useMemo,
11+
useRef,
12+
useState,
13+
} from 'react'
414
import {
515
Badge,
616
Button,
@@ -48,11 +58,13 @@ import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-s
4858
import type { TraceSpan } from '@/lib/logs/types'
4959
import { sendMothershipMessage } from '@/lib/mothership/events'
5060
import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels'
51-
import {
52-
ExecutionSnapshot,
53-
FileCards,
54-
TraceView,
55-
} from '@/app/workspace/[workspaceId]/logs/components'
61+
/**
62+
* Deep imports on purpose: importing these back through the parent `logs/components`
63+
* barrel forms a parent->child cycle that would keep the barrel edge to the snapshot
64+
* alive and silently defeat the ExecutionSnapshot lazy split below.
65+
*/
66+
import { FileCards } from '@/app/workspace/[workspaceId]/logs/components/log-details/components/file-download'
67+
import { TraceView } from '@/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view'
5668
import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks'
5769
import {
5870
logDetailsTabParam,
@@ -73,6 +85,17 @@ import { useLogDetailsUIStore } from '@/stores/logs/store'
7385
import { MAX_LOG_DETAILS_WIDTH_RATIO, MIN_LOG_DETAILS_WIDTH } from '@/stores/logs/utils'
7486
import type { ChatContext } from '@/stores/panel'
7587

88+
/**
89+
* Lazy per the code-splitting rule in `sim-imports.md`: the snapshot renders the workflow
90+
* preview canvas, whose graph is ~7.6 MB of source. Rendering is gated on the detail's
91+
* open state, so the chunk is fetched on first use, never during SSR or hydration.
92+
*/
93+
const ExecutionSnapshot = lazy(() =>
94+
import(
95+
'@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot'
96+
).then((m) => ({ default: m.ExecutionSnapshot }))
97+
)
98+
7699
/**
77100
* Renders an already-apportioned integer credit value. `dollars` is only used
78101
* to distinguish a genuine zero ("0 credits") from a sub-credit charge that
@@ -679,13 +702,15 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
679702

680703
{/* Frozen Canvas Modal */}
681704
{log.executionId && (
682-
<ExecutionSnapshot
683-
executionId={log.executionId}
684-
traceSpans={traceSpans}
685-
isModal
686-
isOpen={isExecutionSnapshotOpen}
687-
onClose={() => setIsExecutionSnapshotOpen(false)}
688-
/>
705+
<Suspense fallback={null}>
706+
<ExecutionSnapshot
707+
executionId={log.executionId}
708+
traceSpans={traceSpans}
709+
isModal
710+
isOpen={isExecutionSnapshotOpen}
711+
onClose={() => setIsExecutionSnapshotOpen(false)}
712+
/>
713+
</Suspense>
689714
)}
690715
</>
691716
)

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use client'
22

33
import {
4+
lazy,
5+
Suspense,
46
useCallback,
57
useEffect,
68
useEffectEvent,
@@ -93,7 +95,7 @@ import { useDebounce } from '@/hooks/use-debounce'
9395
import { useUrlSort } from '@/hooks/use-url-sort'
9496
import { useFilterStore } from '@/stores/logs/filters/store'
9597
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
96-
import { Dashboard, ExecutionSnapshot, LogDetails, LogRowContextMenu } from './components'
98+
import { Dashboard, LogDetails, LogRowContextMenu } from './components'
9799
import {
98100
formatDate,
99101
getDisplayStatus,
@@ -106,6 +108,20 @@ import {
106108
workflowEditorPath,
107109
} from './utils'
108110

111+
/**
112+
* Lazy per the code-splitting rule in `sim-imports.md`: the snapshot renders the workflow
113+
* preview canvas, whose graph is ~7.6 MB of source (the editor's sub-block components and
114+
* the generated tool metadata). Both render sites are gated on client-only state (a preview
115+
* selection / an opened detail), so the chunk is fetched on first use, never during SSR or
116+
* hydration. The now-dead barrel re-export is deleted — with no `sideEffects: false`, a
117+
* leftover re-export would silently defeat this split.
118+
*/
119+
const ExecutionSnapshot = lazy(() =>
120+
import(
121+
'@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot'
122+
).then((m) => ({ default: m.ExecutionSnapshot }))
123+
)
124+
109125
const LOGS_PER_PAGE = 50 as const
110126
const REFRESH_SPINNER_DURATION_MS = 1000 as const
111127
const LIVE_REFRESH_INTERVAL_MS = 10_000 as const
@@ -1259,13 +1275,15 @@ export default function Logs() {
12591275
/>
12601276

12611277
{previewLogId !== null && previewDetailQuery.data?.executionId && (
1262-
<ExecutionSnapshot
1263-
executionId={previewDetailQuery.data.executionId}
1264-
traceSpans={previewDetailQuery.data.executionData?.traceSpans}
1265-
isModal
1266-
isOpen={previewLogId !== null}
1267-
onClose={handleClosePreview}
1268-
/>
1278+
<Suspense fallback={null}>
1279+
<ExecutionSnapshot
1280+
executionId={previewDetailQuery.data.executionId}
1281+
traceSpans={previewDetailQuery.data.executionData?.traceSpans}
1282+
isModal
1283+
isOpen={previewLogId !== null}
1284+
onClose={handleClosePreview}
1285+
/>
1286+
</Suspense>
12691287
)}
12701288
</>
12711289
)

apps/sim/app/workspace/[workspaceId]/skills/components/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import JSZip from 'jszip'
21
import { isApiClientError } from '@/lib/api/client/errors'
32

43
export interface ParsedSkill {
@@ -91,6 +90,12 @@ function inferNameFromHeading(markdown: string): string {
9190
export async function extractSkillFromZip(
9291
data: File | Blob | ArrayBuffer | Uint8Array
9392
): Promise<string> {
93+
/**
94+
* Dynamic on purpose (mirrors `lib/workflows/operations/import-export.ts`): jszip is
95+
* ~28 KB gzip and this user-triggered upload path is the only reason it would sit in
96+
* the initial bundle of every route that links the skills surface.
97+
*/
98+
const { default: JSZip } = await import('jszip')
9499
const zip = await JSZip.loadAsync(data)
95100

96101
const candidates: string[] = []

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,9 @@ import { useUndoRedoStore } from '@/stores/undo-redo'
151151
import { useVariablesModalStore } from '@/stores/variables/modal'
152152
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
153153
import { useWorkflowSearchReplaceStore } from '@/stores/workflow-search-replace/store'
154+
import { prepareBlockState } from '@/stores/workflows/prepare-block-state'
154155
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
155-
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
156+
import { getUniqueBlockName } from '@/stores/workflows/utils'
156157
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
157158
import type { BlockState } from '@/stores/workflows/workflow/types'
158159

apps/sim/lib/api/contracts/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* Deliberately NOT re-exported from this barrel: `./tools` (per-integration tool
3+
* contracts), `./selectors`, `./v1` (admin API), and `./demo-requests`. All of their
4+
* consumers import those files directly, and re-exporting them here shipped their Zod
5+
* schema construction (~60 KB gzip) to every route that touches any contract — schema
6+
* objects are built at module scope, so `export *` defeats tree-shaking for them.
7+
* Import from the specific contract file instead.
8+
*/
19
export * from './admin'
210
export * from './api-keys'
311
export * from './audit-logs'
@@ -7,7 +15,6 @@ export * from './cli-auth'
715
export * from './common'
816
export * from './copilot'
917
export * from './credentials'
10-
export * from './demo-requests'
1118
export * from './desktop-auth'
1219
export * from './desktop-tool-authorization'
1320
export * from './environment'
@@ -23,15 +30,12 @@ export * from './primitives'
2330
export * from './sandboxes'
2431
export * from './secret-mount-policy'
2532
export * from './secrets'
26-
export * from './selectors'
2733
export * from './skills'
2834
export * from './storage-transfer'
2935
export * from './subscription'
3036
export * from './tool-primitives'
31-
export * from './tools'
3237
export * from './types'
3338
export * from './user'
34-
export * from './v1'
3539
export * from './workflows'
3640
export * from './workspace-file-folders'
3741
export * from './workspace-files'

apps/sim/lib/pptx-renderer/parser/zip-parser.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55

66
import type { JSZipObject } from 'jszip'
7-
import JSZip from 'jszip'
87

98
export interface PptxFiles {
109
contentTypes: string
@@ -80,6 +79,9 @@ export async function parseZip(
8079
throwZipLimitExceeded(`maxConcurrency ${limits.maxConcurrency} must be an integer >= 1`)
8180
}
8281

82+
/** Dynamic on purpose — keeps jszip out of the initial bundle of routes that only
83+
* *can* open a PPTX; the archive load below is already async. */
84+
const { default: JSZip } = await import('jszip')
8385
const zip = await JSZip.loadAsync(buffer)
8486
const entries = Object.entries(zip.files).filter(([, file]) => !file.dir)
8587

0 commit comments

Comments
 (0)