2023.10.28
Vue Fes Japan 2023
翠 (@sapphi-red)
2023.10.28
Vue Fes Japan 2023
翠 (@sapphi-red)
Viteのコアチームメンバーとして
どんなことをしているの?
function computation1() { // 何か重い処理 } function computation2() { // 何か重い処理 } const result = computation1() + computation2()
function computation1() { // 何か重い処理 } function computation2() { // 何か重い処理 } const result = computation1() + computation2()
※これらのCPUは多め (4~16が一般的)
JavaScriptはシングルスレッドで実行される
→何らかの回避策を利用
fetch
は「JavaScriptエンジン外で」ネットワーク処理をし、別スレッドに処理を委譲できる基本的に参照の共有ができない
スレッド間のやりとりのたびに、コピーによるオーバーヘッドが発生
シングルスレッド
マルチスレッド
スレッドの境界を越えたあとでは、参照での比較ができない
const foo = { bar: { baz: 'baz' } } const bar = foo.bar console.log(foo.bar === bar) // true console.log(structuredClone(foo.bar) === structuredClone(bar)) // false
const foo = { bar: { baz: 'baz' } } const bar = foo.bar console.log(foo.bar === bar) // true console.log(structuredClone(foo.bar) === structuredClone(bar)) // false
structuredClone
は、Worker threadsやWeb Workerでの通信の際に利用されるコピーと同じ動作をする関数です。コピーできないもの (例: 関数、クラスの情報) はやり取りできない
// 関数 structuredClone(() => {}) // error // クラスの名前 console.log(new Foo().constructor.name) // "Foo" console.log(structuredClone(new Foo()).constructor.name) // "Object"
// 関数 structuredClone(() => {}) // error // クラスの名前 console.log(new Foo().constructor.name) // "Foo" console.log(structuredClone(new Foo()).constructor.name) // "Object"
詳しい仕様はMDNの構造化複製アルゴリズムを参照
→関数の参照を直接別スレッドのエントリーポイントにできない
const workerFunc = () => { return (a, b) => a + b } new Worker(workerFunc) // できない new Worker('(a, b) => a + b', { eval: true }) // 文字列として受け取る new Worker('/path/to/entryfile.js') // ファイルパスとして受け取る
const workerFunc = () => { return (a, b) => a + b } new Worker(workerFunc) // できない new Worker('(a, b) => a + b', { eval: true }) // 文字列として受け取る new Worker('/path/to/entryfile.js') // ファイルパスとして受け取る
コピーせず移動 / 共有できる ArrayBuffer
/ SharedArrayBuffer
が扱えるのはバイト列のみ
→利用するにはバイト列に対する操作に書き換える必要あり
const buff = new SharedArrayBuffer(8) const arr = new Uint32Array(buff) const flags = { get isEsm() { return Atomics.load(arr, 0) === 1 }, set isEsm(v) { Atomics.store(arr, 0, v ? 1 : 0) }, get count() { return Atomics.load(arr, 1) }, set count(v) { Atomics.store(arr, 1, v) }, } console.log(flags.isEsm) // false flags.isEsm = true console.log(flags.isEsm) // true console.log(flags.count) // 0
const buff = new SharedArrayBuffer(8) const arr = new Uint32Array(buff) const flags = { get isEsm() { return Atomics.load(arr, 0) === 1 }, set isEsm(v) { Atomics.store(arr, 0, v ? 1 : 0) }, get count() { return Atomics.load(arr, 1) }, set count(v) { Atomics.store(arr, 1, v) }, } console.log(flags.isEsm) // false flags.isEsm = true console.log(flags.isEsm) // true console.log(flags.count) // 0
ArrayBuffer
向けにはbuffer-backed-object
というライブラリもある
import * as BBO from "buffer-backed-object"; const buffer = new ArrayBuffer(8); const boolean32 = () => { const uint32 = BBO.Uint32() return { ...uint32, get: (dataView, byteOffset) => uint32.get(dataView, byteOffset) === 1, set: (dataView, byteOffset, value) => { uint32.set(dataView, byteOffset, value ? 1 : 0) } } } const flags = BBO.BufferBackedObject(buffer, { isEsm: boolean32(), count: BBO.Uint32() });
import * as BBO from "buffer-backed-object"; const buffer = new ArrayBuffer(8); const boolean32 = () => { const uint32 = BBO.Uint32() return { ...uint32, get: (dataView, byteOffset) => uint32.get(dataView, byteOffset) === 1, set: (dataView, byteOffset, value) => { uint32.set(dataView, byteOffset, value ? 1 : 0) } } } const flags = BBO.BufferBackedObject(buffer, { isEsm: boolean32(), count: BBO.Uint32() });
グローバルで共有する状態への依存
例: TypeScriptのDeclaration Merging (デモ)
// file1.ts interface Foo { foo: string }
// file1.ts interface Foo { foo: string }
// file2.ts interface Foo { bar: string }
// file2.ts interface Foo { bar: string }
// 結果 interface Foo { foo: string bar: string }
// 結果 interface Foo { foo: string bar: string }
ファイルに対する処理がグローバルな「インターフェース情報一覧テーブル」に依存している
関数を持つオブジェクトへの依存
type Config = { filter(path: string): boolean } const run = (config: Config) => { runA(config) runB(config) }
type Config = { filter(path: string): boolean } const run = (config: Config) => { runA(config) runB(config) }
filter
は関数で別スレッドに移せない
参照への依存
class CustomError extends Error {} const run = () => { const result = runA() runB(result) }
class CustomError extends Error {} const run = () => { const result = runA() runB(result) }
const runA = () => { return { result: new CustomError() } } const runB = (result: Result) => { if (result instanceof CustomError) { return 'error' } return 'ok' }
const runA = () => { return { result: new CustomError() } } const runB = (result: Result) => { if (result instanceof CustomError) { return 'error' } return 'ok' }
runB
がCustomError
の参照に依存している
依存関係に注目して、マルチスレッドにする箇所を選択する
依存関係が少なく、処理が多い箇所を見つける
実際に別スレッドを呼び出すように書き換える (ここではNode.jsのworker_threads
を利用)
onMessage
とpostMessage
に書き換え artichokie
okie
をベースにした、いい感じにやるライブラリ
写真: Unsplash
artichokie
利用例const createResolver = (config: Config) => { const rewrite = (target: string) => config.rewrite(target) const worker = new Worker( () => { const path = require('node:path') return async (base: string, target: string) => { const rewrote = await rewrite(target) return path.resolve(base, rewrote) }, } { parentFunction: { rewrite } } ) return worker }
const createResolver = (config: Config) => { const rewrite = (target: string) => config.rewrite(target) const worker = new Worker( () => { const path = require('node:path') return async (base: string, target: string) => { const rewrote = await rewrite(target) return path.resolve(base, rewrote) }, } { parentFunction: { rewrite } } ) return worker }
const resolver = createResolver(config) const result = await resolver.run( '/bar', './baz' ) resolver.stop()
const resolver = createResolver(config) const result = await resolver.run( '/bar', './baz' ) resolver.stop()
ここではlessの部分の変更を紹介
const less: StylePreprocessor = async (source, options, resolvers) => { const nodeLess = require('less') const viteResolverPlugin = createViteLessPlugin( nodeLess, options.filename, options.alias, resolvers ) let result: Less.RenderOutput | undefined try { result = await nodeLess.render(source, { ...options, plugins: [viteResolverPlugin, ...(options.plugins || [])], }) } catch (e) { return { code: '', error: e, deps: [] } } return { code: result.css.toString(), deps: result.imports } }
const less: StylePreprocessor = async (source, options, resolvers) => { const nodeLess = require('less') const viteResolverPlugin = createViteLessPlugin( nodeLess, options.filename, options.alias, resolvers ) let result: Less.RenderOutput | undefined try { result = await nodeLess.render(source, { ...options, plugins: [viteResolverPlugin, ...(options.plugins || [])], }) } catch (e) { return { code: '', error: e, deps: [] } } return { code: result.css.toString(), deps: result.imports } }
ここではlessの部分の変更を紹介
const worker = new WorkerWithFallback( () => { const less = require('less') return async (source: string, options: string) => { const viteResolverPlugin = /* 省略 (viteLessResolveを利用) */ return await nodeLess.render(source, { ...options, plugins: [viteResolverPlugin, ...(options.plugins || [])], }) }, } { parentFunction: { viteLessResolve }, shouldUseFake(source, options) { return options.plugins.length > 0 } } )
const worker = new WorkerWithFallback( () => { const less = require('less') return async (source: string, options: string) => { const viteResolverPlugin = /* 省略 (viteLessResolveを利用) */ return await nodeLess.render(source, { ...options, plugins: [viteResolverPlugin, ...(options.plugins || [])], }) }, } { parentFunction: { viteLessResolve }, shouldUseFake(source, options) { return options.plugins.length > 0 } } )
<style lang="sass"> @import './styles/vars.sass' .foo background-color: $variable-from-vars </style>
<style lang="sass"> @import './styles/vars.sass' .foo background-color: $variable-from-vars </style>
上記のように各Vueコンポーネントでプリプロセッサの処理が少なくない場合に
未マージだが、Vite 5.1での導入を目指している
artichokie
のお試しを