C# Kaigi 2026
JavaScriptエンジン Okojoブラウザ / レンダリングエンジン Falconet
JavaScriptの実行から画面の更新まで、実際に動く実装の話。
Part 1 / Okojo
実行中の仕事を、どう減らしたか
値を置く → 関数を呼ぶ → 検索を再利用する
「使えるエンジンがない」などの需要よりも、自分で作りたかった。
JsValue
比率は Okojo / Jint。1未満ならOkojoの時間・allocationが小さい。
array-stress
linq-js
3d-cube
eval
¹ minimalは3.5× ² evalは9.3×
minimal
2026-09-12 / BenchmarkDotNet ShortRun / Jint 4.16.1
Okojo benchmark (5ee59d1) / Jint.Benchmark (cd4037d)
let identity = function (x) { return x; }; let s = 0; for (let i = 0; i < 5000; i = i + 1) { s = identity(i) + 1; }
ShortRun / TieredPGO=0。測定対象は、5000回のcallを含むloop全体。
for (let i = 1; i <= 2000; i++) { s += Math.sin(i) + Math.cos(i) + Math.sqrt(i); s += Math.log(i) + Math.pow(i, 0.5) + Math.imul(i, 3); }
Math.trunc / log2 / log10と定数参照も含む。Okojo / Jint = 0.53。
Math.trunc
log2
log10
Span<T>
参考にしたarchitectureを、C# / CLR GC / JITの上でどう実装したか。
function add(a, b) { return a + b; } add(20, 22);
20
22
add
42
値の置き場所と、call / returnの実装を見ていく。
function add(a, b) { return a + b; }
Ldar r1
r1 = 22
accumulator = 22
Add r0
r0 = 20
accumulator = 42
Return
register
accumulator
ここでのregisterはVM上のslot。CPUの物理registerとは別。
CallFrame header + registersを、同じJsValue[]の後ろへ積む。
JsValue[]
Run(script)
Run(add)
Run(foo)
fp
pc
CallerFp
CallerPc
通常の同一Realm内bytecode callを対象とする。
JavaScriptのcall stackは、自前のframe stackで管理する。
foo(10, 20, 30);
public ReadOnlySpan<JsValue> Arguments => Realm.Stack.AsSpan(ArgumentOffset, ArgumentCount);
CallInfo
callback用の追加配列と、そこへ詰め直すcopyをなくす。
NaN boxingで、同じ8 bytesをdoubleのbit列またはtag付きの値として使う。
target[i] = source[i];
object? Obj
CORINFO_HELP_ASSIGN_BYREF
runtime / tier / 代入先によって生成codeは変わる。
16 bytesのstructでも、単純なbit列のcopyとは限らない。
Unsafe.AsRef(in dst.U) = src.U; if (src.Obj is null) { Unsafe.AsRef(in dst.Obj) = null; } else { Unsafe.AsRef(in dst.Obj) = src.Obj; }
U
Obj is null
for (var i = 0; i < source.Length; i++) Copy(ref target[i], in source[i]);
1,024要素 / 2026-09-17。reduced probeの結果であり、engine全体の改善率ではない。
item.score
let total = 0; for (const item of items) { total += item.score; }
score
Dictionary<string, …>
まず、初回に名前から値を取り出す経路を見る。
"id"
"name"
"score"
文字列の比較を、整数IDの比較へ変える。
const item = { id: 7, name: "apple", score: 80 };
const a = { id: 1, name: "A", score: 80 }; const b = { id: 2, name: "B", score: 95 };
同じShapeならproperty配置も同じで、scoreは同じslotにある。
total += item.score;
初回は Atomから検索。cache hitでは Shapeを確認してslotへ直行。
Part 2 / Falconet
DOM / CSSを変えたあと、どこまでやり直すか
差分を調べる → 影響を伝える → 必要な範囲を再計算
box.style.color = "red"; box.style.width = "240px";
color
width
どちらもCSSの変更。でも、やり直す仕事は同じではない。
毎回すべての工程をやり直すのではなく、前回の結果を使う。
StyleId #120
StyleId #145
同値は共有する。変わった部分から、後段への影響を分類する。
「CSSが変わった」だけでなく、何が変わったかを渡す。
nodeごとにclassやListを増やさず、状態と子IDをarenaへまとめる。
var box = _styleStore.GetBox( _nodes[index].Style.Box); box.Size = new SizeDimensions( width, box.Size.Height); UpdateBoxStyle( index, LayoutDependency.Width, ref box);
SelfWidth
Descendant
None
Width / Height / Intrinsic … 変更内容を残すことで、消すcacheを絞れる。
今回の変更:Dirty = Width
Dirty = Width
Width | Intrinsic
Height
(dirtyDependencies & entry.Dependencies) != 0
cacheを保持できても、再利用にはinput constraintなどの確認が必要。
変更がないだけでなく、再利用の条件が成立するかを確認する。
FrameData
実行速度の比較ではなく、使いたいlibraryと開発単位でboundaryを選んだ。
値の表現と再利用の単位を変え、実行時の仕事そのものを減らす。
C# Kaigi 2026 / akeit0
OkojoとFalconetの実装は、上のrepositoryで公開しています。