perf(frontend): split vendor chunks and lazy-load AI SDKs

Two bundle-size wins for the frontend, focused on first-paint for logged-in
users (the production build is 83 MB across 624 JS files today):

- Configure Vite manualChunks so Monaco, ag-grid/charts, AI SDKs, markdown,
  @xyflow+d3-dag, yjs, pdfjs, chart.js, quicktype and quill each land in a
  single cacheable vendor chunk instead of being fragmented across route
  chunks. No byte reduction, but users stop redownloading Monaco on every
  navigation and the number of JS files drops from 624 to 583.

- Convert the OpenAI and Anthropic SDK imports in copilot/lib.ts (and the
  chat/* helpers) to dynamic imports. The SDKs were previously imported
  eagerly through copilot/lib -> aiStore -> sidebar/WorkspaceMenu, so every
  logged-in page paid ~41 KB gzipped for code only used by Copilot. The
  SDKs now live in a separate chunk loaded on first construction of a
  client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-04-24 12:54:05 +00:00
co-authored by Claude Opus 4.7
parent d60adac494
commit 54e1862909
6 changed files with 91 additions and 55 deletions
@@ -416,6 +416,10 @@ class AIChatManager {
// on each iteration. This is critical for changeModeTool (Navigator → Script/Flow)
// which reassigns this.tools, this.helpers, this.systemMessage mid-loop.
const self = this
const [openai, anthropic] = await Promise.all([
workspaceAIClients.getOpenaiClient(),
workspaceAIClients.getAnthropicClient()
])
const result = await runChatLoop({
messages,
get systemMessage() {
@@ -433,8 +437,8 @@ class AIChatManager {
return getCurrentModel()
},
clients: {
openai: workspaceAIClients.getOpenaiClient(),
anthropic: workspaceAIClients.getAnthropicClient()
openai,
anthropic
},
workspace: get(workspaceStore) ?? '',
skipResponsesApi: this.skipResponsesApi,
@@ -1,5 +1,5 @@
import { OpenAI } from 'openai'
import Anthropic from '@anthropic-ai/sdk'
import type { OpenAI } from 'openai'
import type Anthropic from '@anthropic-ai/sdk'
import type {
ChatCompletionMessageParam,
ChatCompletionMessageFunctionToolCall
@@ -41,7 +41,7 @@ export async function getAnthropicCompletion(
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
const client = options?.anthropicClient ?? workspaceAIClients.getAnthropicClient()
const client = await (options?.anthropicClient ?? workspaceAIClients.getAnthropicClient())
const anthropicParams = {
model: config.model,
@@ -1,5 +1,5 @@
import OpenAI from 'openai'
import Anthropic from '@anthropic-ai/sdk'
import type OpenAI from 'openai'
import type Anthropic from '@anthropic-ai/sdk'
import type {
ChatCompletionMessageParam,
ChatCompletionSystemMessageParam,
@@ -8,16 +8,9 @@ import type {
import type { AIProviderModel } from '$lib/gen'
import { getCompletion, parseOpenAICompletion } from '../lib'
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
import {
getOpenAIResponsesCompletion,
parseOpenAIResponsesCompletion
} from './openai-responses'
import { getOpenAIResponsesCompletion, parseOpenAIResponsesCompletion } from './openai-responses'
import type { Tool, ToolCallbacks } from './shared'
import {
addChatTokenUsage,
emptyChatTokenUsage,
type ChatTokenUsage
} from './tokenUsage'
import { addChatTokenUsage, emptyChatTokenUsage, type ChatTokenUsage } from './tokenUsage'
export interface ChatClients {
openai: OpenAI
@@ -137,10 +130,7 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
break
}
} catch (err) {
console.warn(
'OpenAI Responses API failed, falling back to Completions API:',
err
)
console.warn('OpenAI Responses API failed, falling back to Completions API:', err)
const errorMessage = err instanceof Error ? err.message : String(err)
if (errorMessage.includes('Responses API is not enabled')) {
skipResponsesApi = true
@@ -172,15 +162,10 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
}
}
} else if (isAnthropic) {
const completion = await getAnthropicCompletion(
messageParams,
abortController,
toolDefs,
{
forceModelProvider: modelProvider,
anthropicClient: clients.anthropic
}
)
const completion = await getAnthropicCompletion(messageParams, abortController, toolDefs, {
forceModelProvider: modelProvider,
anthropicClient: clients.anthropic
})
if (completion) {
const continueCompletion = await parseAnthropicCompletion(
completion,
@@ -1,4 +1,5 @@
import OpenAI, { OpenAIError } from 'openai'
import type OpenAI from 'openai'
import type { OpenAIError } from 'openai'
import type {
ChatCompletionMessageParam,
ChatCompletionMessageFunctionToolCall,
@@ -14,10 +15,7 @@ import {
import { processToolCall, type Tool, type ToolCallbacks } from './shared'
import type { ResponseStream } from 'openai/lib/responses/ResponseStream.mjs'
import type { AIProviderModel } from '$lib/gen'
import {
openAIResponsesUsageToChatTokenUsage,
type ChatTokenUsage
} from './tokenUsage'
import { openAIResponsesUsageToChatTokenUsage, type ChatTokenUsage } from './tokenUsage'
interface ParsedCompletionResult {
shouldContinue: boolean
@@ -149,7 +147,7 @@ export async function getOpenAIResponsesCompletion(
const { instructions, input } = convertMessagesToResponsesInput(messages)
const responsesConfig = convertCompletionConfigToResponsesConfig(config)
const client = options?.openaiClient ?? workspaceAIClients.getOpenaiClient()
const client = await (options?.openaiClient ?? workspaceAIClients.getOpenaiClient())
const runner = client.responses.stream(
{
@@ -178,7 +176,7 @@ export async function* getOpenAIResponsesCompletionStream(
const { instructions, input } = convertMessagesToResponsesInput(messages)
const responsesConfig = convertCompletionConfigToResponsesConfig(config)
const openaiClient = workspaceAIClients.getOpenaiClient()
const openaiClient = await workspaceAIClients.getOpenaiClient()
const runner = openaiClient.responses.stream(
{
@@ -417,11 +415,11 @@ export async function getNonStreamingOpenAIResponsesCompletion(
}
}
const openaiClient = testOptions?.apiKey
const openaiClient = await (testOptions?.apiKey
? createOpenAIProxyClient(getAiProxyBaseURL())
: testOptions?.workspace
? workspaceAIClients.createOpenaiClient(testOptions.workspace)
: workspaceAIClients.getOpenaiClient()
: workspaceAIClients.getOpenaiClient())
const response = await openaiClient.responses.create(
{
+19 -17
View File
@@ -1,7 +1,7 @@
import type { AIProvider, AIProviderModel } from '$lib/gen'
import { workspaceStore, type DBSchema, type SQLSchema } from '$lib/stores'
import type { IntrospectionQuery } from 'graphql'
import OpenAI from 'openai'
import type OpenAI from 'openai'
import type {
ChatCompletionChunk,
ChatCompletionCreateParams,
@@ -10,7 +10,7 @@ import type {
ChatCompletionMessageFunctionToolCall,
ChatCompletionMessageParam
} from 'openai/resources/index.mjs'
import Anthropic from '@anthropic-ai/sdk'
import type Anthropic from '@anthropic-ai/sdk'
import { get, type Writable } from 'svelte/store'
import { OpenAPI, ResourceService, type Script } from '../../gen'
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
@@ -389,8 +389,9 @@ export function getAiProxyBaseURL(workspace?: string): string {
: `${location.origin}${OpenAPI.BASE}/ai/proxy`
}
export function createOpenAIProxyClient(baseURL: string): OpenAI {
return new OpenAI({
export async function createOpenAIProxyClient(baseURL: string): Promise<OpenAI> {
const { default: OpenAIClass } = await import('openai')
return new OpenAIClass({
baseURL,
apiKey: 'fake-key',
defaultHeaders: {
@@ -400,8 +401,9 @@ export function createOpenAIProxyClient(baseURL: string): OpenAI {
})
}
export function createAnthropicProxyClient(baseURL: string): Anthropic {
return new Anthropic({
export async function createAnthropicProxyClient(baseURL: string): Promise<Anthropic> {
const { default: AnthropicClass } = await import('@anthropic-ai/sdk')
return new AnthropicClass({
baseURL,
apiKey: 'fake-key',
dangerouslyAllowBrowser: true,
@@ -410,30 +412,30 @@ export function createAnthropicProxyClient(baseURL: string): Anthropic {
}
class WorkspacedAIClients {
private openaiClient: OpenAI | undefined
private anthropicClient: Anthropic | undefined
private openaiClient: Promise<OpenAI> | undefined
private anthropicClient: Promise<Anthropic> | undefined
init(workspace: string) {
this.openaiClient = this.createOpenaiClient(workspace)
this.anthropicClient = this.createAnthropicClient(workspace)
}
createOpenaiClient(workspace: string): OpenAI {
createOpenaiClient(workspace: string): Promise<OpenAI> {
return createOpenAIProxyClient(getAiProxyBaseURL(workspace))
}
createAnthropicClient(workspace: string): Anthropic {
createAnthropicClient(workspace: string): Promise<Anthropic> {
return createAnthropicProxyClient(getAiProxyBaseURL(workspace))
}
getOpenaiClient() {
getOpenaiClient(): Promise<OpenAI> {
if (!this.openaiClient) {
throw new Error('OpenAI not initialized')
}
return this.openaiClient
}
getAnthropicClient() {
getAnthropicClient(): Promise<Anthropic> {
if (!this.anthropicClient) {
throw new Error('Anthropic not initialized')
}
@@ -522,11 +524,11 @@ async function testAnthropicKey({
headers['X-API-Key'] = apiKey
}
const anthropicClient = apiKey
const anthropicClient = await (apiKey
? createAnthropicProxyClient(getAiProxyBaseURL())
: workspace
? workspaceAIClients.createAnthropicClient(workspace)
: workspaceAIClients.getAnthropicClient()
: workspaceAIClients.getAnthropicClient())
await anthropicClient.messages.create(
{
@@ -800,11 +802,11 @@ export async function getNonStreamingCompletion(
'X-API-Key': testOptions.apiKey
}
}
const openaiClient = testOptions?.apiKey
const openaiClient = await (testOptions?.apiKey
? createOpenAIProxyClient(getAiProxyBaseURL())
: testOptions?.workspace
? workspaceAIClients.createOpenaiClient(testOptions.workspace)
: workspaceAIClients.getOpenaiClient()
: workspaceAIClients.getOpenaiClient())
const completion = await openaiClient.chat.completions.create(config, fetchOptions)
response = completion.choices?.[0]?.message.content || ''
@@ -911,7 +913,7 @@ export async function getCompletion(
}
// Use Completions API for other providers
const client = options?.openaiClient ?? workspaceAIClients.getOpenaiClient()
const client = await (options?.openaiClient ?? workspaceAIClients.getOpenaiClient())
const completionConfig =
(provider === 'openai' || provider === 'azure_openai' || provider === 'googleai') &&
config.stream
+47
View File
@@ -111,6 +111,53 @@ const config = {
dedupe: ['vscode', 'monaco-editor']
},
assetsInclude: ['**/*.wasm'],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('monaco-editor') || id.includes('@codingame/monaco-vscode')) {
return 'vendor-monaco'
}
if (id.includes('/openai/') || id.includes('/@anthropic-ai/')) {
return 'vendor-ai'
}
if (id.includes('@xyflow/svelte') || id.includes('d3-dag')) {
return 'vendor-flow'
}
if (id.includes('/yjs/') || id.includes('y-websocket') || id.includes('y-monaco')) {
return 'vendor-collab'
}
if (id.includes('ag-grid') || id.includes('ag-charts')) {
return 'vendor-ag'
}
if (id.includes('pdfjs-dist')) {
return 'vendor-pdf'
}
if (
id.includes('svelte-exmarkdown') ||
id.includes('/parse5/') ||
id.includes('/rehype-') ||
id.includes('/remark-') ||
id.includes('/mdast-') ||
id.includes('/micromark')
) {
return 'vendor-markdown'
}
if (id.includes('quicktype-core')) {
return 'vendor-quicktype'
}
if (id.includes('/quill/')) {
return 'vendor-quill'
}
if (id.includes('/chart.js/') || id.includes('chartjs-')) {
return 'vendor-chart'
}
}
}
}
}
},
test: {
expect: { requireAssertions: true },
projects: [