Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ const sentry =
})
: false

// Every module the entry reaches through static imports lands in one chunk. Automatic splitting
// otherwise fragments the initial graph into ~50 files shared with lazy routes, and each file costs
// the renderer a main-thread request round trip through the main process before first paint.
type ChunkingContext = { getModuleInfo(id: string): { isEntry: boolean; importers: readonly string[] } | null }
const initialGraph = new WeakMap<ChunkingContext, Map<string, boolean>>()
function inInitialGraph(id: string, ctx: ChunkingContext) {
const memo = initialGraph.get(ctx) ?? new Map<string, boolean>()
initialGraph.set(ctx, memo)
const visit = (id: string, path: Set<string>): boolean => {
const known = memo.get(id)
if (known !== undefined) return known
if (path.has(id)) return false
const info = ctx.getModuleInfo(id)
if (!info) return false
path.add(id)
const result = info.isEntry || info.importers.some((importer) => visit(importer, path))
path.delete(id)
memo.set(id, result)
return result
}
return visit(id, new Set())
}

export default defineConfig(({ command }) => ({
main: {
resolve: {
Expand Down Expand Up @@ -110,6 +133,11 @@ const require = __cjs_mod__.createRequire(import.meta.url);
input: {
main: "src/renderer/index.html",
},
output: {
codeSplitting: {
groups: [{ name: (id, ctx) => (inInitialGraph(id, ctx) ? "app" : null), priority: 10 }],
},
},
},
},
},
Expand Down
16 changes: 11 additions & 5 deletions packages/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import { marks } from "./lifecycle/marks"
import { app } from "electron"
import { acquireApplicationLock, configureApplication } from "./lifecycle/configure"
import { createEarlyWindow } from "./windows/early"
import { createEarlyWindow } from "./windows/early"
import { rendererAssetsServed } from "./windows/protocol"
import { registerRendererScheme } from "./windows/scheme"

// This module stays small on purpose. Electron holds the ready event until the entry module has
Expand All @@ -14,10 +15,15 @@ if (acquireApplicationLock()) {
registerRendererScheme()
// Window first, then the bundle: starting the import before ready delays ready itself, because the
// module graph evaluates on the same thread Chromium needs to finish initialising.
void app.whenReady().then(() => {
marks.ready = Date.now()
createEarlyWindow()
marks.window = Date.now()
void app.whenReady().then(async () => {
marks.ready = Date.now()
createEarlyWindow()
marks.window = Date.now()
// The window's renderer is already loading. Its HTML and preloaded chunks are served from this
// thread, so the bundle waits for that burst to be answered (or a cap) before it evaluates.
if (!process.env.ELECTRON_RENDERER_URL) await rendererAssetsServed({ quietMs: 40, capMs: 400 })
marks.served = Date.now()
return import("./desktop")
})
}

31 changes: 29 additions & 2 deletions packages/desktop/src/main/windows/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,39 @@ export function setProtocolReporter(reporter: ProtocolReport) {
report = reporter
}

// Requests in flight and when the last one arrived. The entry module holds the main bundle back
// until the renderer's initial burst of asset requests has been answered, because this handler
// runs on the main thread and a 100 ms module evaluation would otherwise sit between the renderer
// and its HTML.
let inflight = 0
let served = 0
let lastRequest = 0

export function rendererAssetsServed(options: { quietMs: number; capMs: number }) {
const start = Date.now()
return new Promise<void>((resolve) => {
const check = () => {
const now = Date.now()
if (now - start >= options.capMs) return resolve()
if (served > 0 && inflight === 0 && now - lastRequest >= options.quietMs) return resolve()
setTimeout(check, 5)
}
check()
})
}

export function registerRendererProtocol(rendererRoot: string) {
if (protocol.isProtocolHandled(rendererProtocol)) return

protocol.handle(rendererProtocol, async (request) => {
return serve(request, rendererRoot)
inflight++
lastRequest = Date.now()
try {
return await serve(request, rendererRoot)
} finally {
inflight--
served++
}
})
}

Expand Down Expand Up @@ -60,4 +88,3 @@ function addDocumentPolicy(response: Response, file: string) {
headers.set(documentPolicyHeader, jsCallStacksDocumentPolicy)
return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
}

Loading