fix(web): speed up lazy bundle loads in the Web preview

The Web preview Worker converted multi-megabyte combo scripts and source
maps with the buffer package's JavaScript UTF-8 codec, and the page
re-encoded each source map to Base64 in JavaScript before running the
bundle. Use TextEncoder/TextDecoder for whole-buffer UTF-8 conversions
and FileReader for the inline source-map data URL.
This commit is contained in:
Turtle committed 2026-10-22 14:32:35 +08:00
1 parent b04c1c3261
commit 4b1d4c814e
4 files changed
+98 -11

No files matched your search

@@ -109,16 +109,18 @@ const REFUSAL_STATUS = 500
const encoder = new TextEncoder()
const SOURCE_MAP_TRAILER = /\/\/# sourceMappingURL=([^\r\n]+)\s*$/
const BASE64_CHUNK_BYTES = 32 * 1024
/** Encode UTF-8 text for an inline data URL without a call-stack-sized spread. */
function base64(value: string): string {
const bytes = encoder.encode(value)
let binary = ''
for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK_BYTES) {
binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK_BYTES))
}
return btoa(binary)
/**
* Encode source-map bytes as a Base64 data URL with the browser's native encoder.
* DevTools cannot load a `blob:` source map, so the map must be inline.
*/
function sourceMapDataUrl(bytes: ArrayBuffer): Promise<string> {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.addEventListener('load', () => { resolve(reader.result as string) }, { once: true })
reader.addEventListener('error', () => { reject(reader.error ?? new Error('source map encoding failed')) }, { once: true })
reader.readAsDataURL(new Blob([bytes], { type: 'application/json;charset=utf-8' }))
})
}
/** Replace a tunnel-only map reference with a self-contained Base64 data URL. */
@@ -128,7 +130,7 @@ async function localizeSourceMap(source: string, bundleUrl: string, fetch: Tunne
try {
const response = await fetch(new URL(match[1], new URL(bundleUrl, globalThis.location.origin)))
if (!response.ok) return source.replace(SOURCE_MAP_TRAILER, '')
const dataUrl = `data:application/json;charset=utf-8;base64,${base64(await response.text())}`
const dataUrl = await sourceMapDataUrl(await response.arrayBuffer())
return source.replace(SOURCE_MAP_TRAILER, `//# sourceMappingURL=${dataUrl}`)
} catch {
// A source map is diagnostic-only; its transport failure must not prevent
@@ -2,9 +2,41 @@
* `node:buffer` for the worker, backed by the `buffer` npm package (feross), and
* the matching `globalThis.Buffer` install. Node code treats Buffer as ambient,
* so the global must exist before any host module evaluates.
*
* Whole-buffer UTF-8 conversions, `Buffer.from(string)` and `buf.toString()`,
* use the browser's `TextEncoder` and `TextDecoder`: they apply Node's U+FFFD
* replacement rules and run several times faster than the package's JavaScript
* codec on multi-megabyte bundles and source maps. Other encodings and ranged
* `toString` calls keep the package's implementation.
*/
import { Buffer, kMaxLength } from 'buffer'
const encoder = new TextEncoder()
// Node keeps a leading byte order mark in the decoded string.
const decoder = new TextDecoder('utf-8', { ignoreBOM: true })
const UTF8 = /^utf-?8$/i
const packageFrom = Buffer.from.bind(Buffer)
const bufferPrototype = Buffer.prototype as Buffer
// oxlint-disable-next-line typescript/unbound-method -- Reflect.apply supplies the Buffer receiver.
const packageToString = bufferPrototype.toString
Buffer.from = function from(value: unknown, encodingOrOffset?: unknown, length?: unknown): Buffer {
if (typeof value === 'string' && (encodingOrOffset === undefined || (typeof encodingOrOffset === 'string' && UTF8.test(encodingOrOffset)))) {
const bytes = encoder.encode(value)
return packageFrom(bytes.buffer, bytes.byteOffset, bytes.byteLength)
}
return Reflect.apply(packageFrom, Buffer, [value, encodingOrOffset, length]) as Buffer
} as typeof Buffer.from
bufferPrototype.toString = function toString(this: Buffer, encoding?: BufferEncoding, start?: number, end?: number): string {
if (start === undefined && end === undefined && (encoding === undefined || UTF8.test(encoding))) {
return decoder.decode(this)
}
return Reflect.apply(packageToString, this, [encoding, start, end])
}
// oxlint-disable-next-line typescript/unbound-method -- Installed as a Buffer method; callers supply the receiver.
Reflect.set(bufferPrototype, 'toLocaleString', bufferPrototype.toString)
Object.defineProperty(globalThis, 'Buffer', { value: Buffer, writable: true, configurable: true })
export { Buffer, kMaxLength }
@@ -68,7 +68,7 @@ it('loads a combo map through the tunnel and embeds it in the blob script', asyn
})
await vi.waitFor(() => { expect(sent).toHaveLength(2) })
expect(sent[1]?.url).toBe(`http://localhost:3000/plugins/${mapUrl}`)
const map = '{"version":3,"sections":[]}'
const map = '{"version":3,"names":["中文"],"sections":[]}'
deliver({
t: 'res',
id: 2,
@@ -0,0 +1,53 @@
/**
* Differential check of the worker's `node:buffer` UTF-8 conversions against Node.
*/
import { Buffer as NodeBuffer } from 'node:buffer'
import { afterAll, expect, it } from 'vitest'
const ambient = globalThis.Buffer
const { Buffer: WorkerBuffer } = await import('@deepseek-ai/dsh-experimental-webworker-runtime/src/node/builtin_modules/implemented/buffer.ts')
afterAll(() => {
Object.defineProperty(globalThis, 'Buffer', { value: ambient, writable: true, configurable: true })
})
const STRINGS = ['', 'a', '中文字符串', '\uFEFFbom', 'lone \uD800 high', 'lone \uDC00 low', '😀 pair', 'x'.repeat(100_000)]
const BYTES = [
[0xEF, 0xBB, 0xBF, 0x61],
[0xC3],
[0xE4, 0xB8],
[0xF0, 0x9F, 0x98],
[0xED, 0xA0, 0x80],
[0xC0, 0x80],
[0xFF, 0xFE, 0x61],
[0x61, 0xF4, 0x90, 0x80, 0x80, 0x62],
]
// Node matches encoding names case-insensitively; its type lists lower-case names only.
const UTF8_NAMES = [undefined, 'utf8', 'utf-8', 'UTF-8'].map(name => name as BufferEncoding | undefined)
it('encodes UTF-8 strings to the bytes Node produces', () => {
for (const value of STRINGS) {
for (const encoding of UTF8_NAMES) {
expect(WorkerBuffer.isBuffer(WorkerBuffer.from(value, encoding))).toBe(true)
expect([...WorkerBuffer.from(value, encoding)]).toEqual([...NodeBuffer.from(value, encoding)])
}
}
})
it('decodes UTF-8 bytes to the string Node produces', () => {
for (const bytes of [...BYTES, ...STRINGS.map(value => [...NodeBuffer.from(value)])]) {
const buffer = WorkerBuffer.from(bytes)
for (const encoding of UTF8_NAMES) {
expect(buffer.toString(encoding)).toBe(NodeBuffer.from(bytes).toString(encoding))
}
expect(buffer.toLocaleString()).toBe(NodeBuffer.from(bytes).toString())
}
})
it('keeps the package conversions for other encodings and ranges', () => {
expect([...WorkerBuffer.from('ÿa', 'latin1')]).toEqual([0xFF, 0x61])
expect([...WorkerBuffer.from('ff61', 'hex')]).toEqual([0xFF, 0x61])
expect(WorkerBuffer.from('中文').toString('hex')).toBe(NodeBuffer.from('中文').toString('hex'))
expect(WorkerBuffer.from('a中b').toString('utf8', 1, 4)).toBe('中')
const view = new Uint8Array([0x61, 0x62, 0x63]).buffer
expect(WorkerBuffer.from(view, 1, 1).toString()).toBe('b')
})