Slide 1

Slide 1 text

極限環境で最終ビルドを絞るための フロントエンド設計 @mizchi | 3rdparty.js #1

Slide 2

Slide 2 text

自己紹介 @mizchi | 竹馬光太郎 株式会社 Plaid ソフトウェアエンジニア Node.js / TypeScript フロントエンドというより CI やビルドパイプライン、静的解析

Slide 3

Slide 3 text

今日の内容 パフォーマンスバジェット 設計の意思決定 UI コンポーネントとビジュアルエディタ 3rd party script: 実装編 3rd party script: 最適化編 型レベル最適化の試み

Slide 4

Slide 4 text

経緯 : フリーランス => Plaid に入る時 某社の偉い人「KARTE 便利だけど重いから速くしておいて」 俺「気が向いたら... 」 社内「解析サーバー再設計と同時に埋め込みタグも見直す(2019) 」 俺「やるか... 」 KARTE タグV2( 社内コード: Edge) と呼ばれているものを実装した話 https://support.karte.io/post/7E5yZwHWroaDTDmd4f0SDx

Slide 5

Slide 5 text

KARTE について マーケター向けの分析と接客施策のツール エンジニア向けの( 端折った) 説明 KARTE における接客 = 何らかのスクリプト実行 リアルタイムに 全ユーザー個別の各種指標を計算する ユーザー個別に 条件を満たした時にスクリプトを配信できる ( 内部的にはアクションと呼称)

Slide 6

Slide 6 text

3rd party script の パフォーマンスバジェット

Slide 7

Slide 7 text

最初に ビルドサイズに言及する際は minify 後かつ gzip 前 を参照 ネットワーク負荷は圧縮後だが... CPU 負荷は圧縮前のコードと比例しやすい ※ bundlephovia.com で最新版を参照するが treeshake 考慮外なことが ある

Slide 8

Slide 8 text

Core Web Vitals Google が SEO のスコアとして用いるパフォーマンス指標 https://web.dev/i18n/ja/learn-core-web-vitals/ パフォーマンスが非機能要件からSEO という機能要件へ LCP: メインコンテンツが描画されるまでの指標

Slide 9

Slide 9 text

3rd party のパフォーマンスバジェット 例えば webpack 推奨の 244kb ... はアプリケーション全体の話 Core Web Vitals への影響は可能な限り避けたい。結論からいうと ~30kb(gzip 前 ) ほどを目安に考える $ webpack WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). This can impact web performance. Assets: main.js (2.82 MiB) WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.

Slide 10

Slide 10 text

KARTE の解析サーバー視点 単純に初期化に時間が掛かると、データを取りこぼす可能性が高い 4G/5G はそもそも不安定なので、余計な実行パスを避けたい

Slide 11

Slide 11 text

3rd party script の設計と意思決定

Slide 12

Slide 12 text

KARTE の埋め込みタグの機能 ( ※一部 ) 送信 : ユーザーとページの情報を KARTE のサーバーに送信 再送 : ネットワーク状態が悪いときに再試行 アクション : 管理画面で設定された条件を満たすと、指定された UI を表示 プラグイン : KARTE の契約状態に基づいて、各種プラグインの実行

Slide 13

Slide 13 text

KARTE の埋め込みタグの機能 : 技術的に分解 document.cookie や location 等からデータを取り出す MutationObserver で DOM 操作を検知 ブラウザストレージに送信キューを保存 fetch() で解析サーバーに届いたことを確認してキューを削除 解析サーバーのレスポンスに応じて DOM に UI を展開

Slide 14

Slide 14 text

旧バージョン (v1) の反省 すべての要望が単一のJS に同居し、実際にはほぼデッドコード化 IE フォールバック用の処理/ ライブラリが大きい 一部のプラグインが自身の初期化に全体ブロッキングを要求

Slide 15

Slide 15 text

v1 の初期化フロー

Slide 16

Slide 16 text

v2 の意思決定 デッドコードの排除 利用単位ごとに .js を個別にビルドする仕組みを提供 管理画面の設定変更や契約状態に応じて再ビルドする ブロッキングの排除 初回リクエストを送るまでのクリティカルパスを必ず1RTT に 内部をプラグインに分割し 遅延 かつ 並列 に起動させる パフォーマンス目標の設定 解析: 空ページに読み込んで Lighthouse 100 点 UI: LCP に関与しても Lighthouse 90 点台

Slide 17

Slide 17 text

デッドコードの排除 利用単位で必要なコードだけに絞って事前ビルド プロジェクト設定を RemoteConfig というインターフェースで抽象 RemoteConfig を元に定数展開しつつ rollup + terser でビルド 本体更新時には全スクリプトの再ビルド= リリース速度になるの で、ビルドパイプラインを可能な限り高速化しておく

Slide 18

Slide 18 text

デッドコードの排除 : 共通テンプレート部 import { loadFeatA, loadFeatB } from "./features"; async function main() { if ($USE_FEAT_A) await loadFeatA(); if ($USE_FEAT_B) await loadFeatB(); } main(); 定数展開 + rollup + terser によって不要 import が取り除かれてビル ドされる 高速化のために共通テンプレート以外は事前にビルドしておく

Slide 19

Slide 19 text

デッドコードの排除 : 定数展開 // 管理画面の更新からリリースまでの擬似コード await updateRemoteConfig(config as RemoteConfig); const constants = expandConstantsFromRemoteConfig(config); const builtJs = await build({ constants, ... }); await release(config.apiKey, builtJs); terser ではネストしたオブジェクトを追跡しきれないのでフラット な定数に展開( 詳しくは後述) 内部的には @rollup/plugin-replace を使っているが、最終的にDCE できればなんでもいい

Slide 20

Slide 20 text

ブロッキングの排除 : プラグインシステムの設計 初回の解析後の処理は、いくつかの処理に分岐する Talk: チャットによるサポート機能 LIVE: 画面操作のリアルタイム録画 etc... 流石にすべてサポートできないので、プラグインシステムを作って 各オーナーに対応をお願いした ( コンウェイの法則 )

Slide 21

Slide 21 text

ブロッキングの排除 : プラグインシステムの設計 // 社内用の共通型定義ファイルから型を提供 type PluginOptions = {/* 共通機能の定義*/, storage: Storage; }; type Plugin = (options: PluginOptions) => () => void; // 実装側 export default (options) => () => {}; 各プロジェクトでこのインターフェースを満たしたスクリプトを CDN にデプロイしておき、RemoteConfig に書き込む const { default: plugin } = await import(plugin.url); plugin(opts);

Slide 22

Slide 22 text

ブロッキングの排除 : v1 での発生の経緯 プラグイン側の要望 「初回起動時からの内部イベントが確実に全部ほしい」 共通スクリプトなのでサーバー上の設定を参照(+1 RTT) 解決策 プラグインマネージャが起動からの内部イベントを溜め込む プラグイン起動時に起動時からのイベントをすべて渡す ( 実質的に社内調整フェーズ)

Slide 23

Slide 23 text

ブロッキングの排除 : タグ部分 <script> // 簡略化したもの window.krt=(...args)=>{ // krt.x を非同期に初期化。初期化までは krt.q のキューに保持 krt.x?.call(null,...args) ?? krt.q.push(args) }; krt.q = []; 自身も同期ブロックせずに非同期で起動(module は常に async) 初期化前の呼び出しを内部のキューに貯めておく

Slide 24

Slide 24 text

v2: 最終的なクリティカルパス 最速でリクエストを飛ばし、他は並列に起動

Slide 25

Slide 25 text

3rd party script 設計 : まとめ ブロッキングを徹底的に排除しウォーターフォールを最適化 デッドコードの排除に個別ビルドを行うことにした コア部分はプラグインの管理に徹する ( 当然だが設計=> 実装ではなく実装+ 計測しながら再設計している)

Slide 26

Slide 26 text

UI コンポーネントと ビジュアルエディターコードエディタ

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

KARTE の ビジュアルエディタ ActionEditor v2 と呼ばれている部分 基本はマーケターが使うことを想定 https://support.karte.io/docs/campaign/editorv2

Slide 29

Slide 29 text

ビジュアルエディタとしての要件 ビジュアルエディタでの編集 基本は定数・色の編集や画像の差し替えを想定 リアルタイムプレビューが必要 コードの直接編集 ビジュアルエディタで実現できないとき、マーケターから依頼さ れることを想定 ここに複雑な技術前提を置きたくない( 一般的なWeb 制作を想定)

Slide 30

Slide 30 text

UI コンポーネント含むパフォーマンス目標 経路: HTML => => KARTE: 解析サーバー => アクション 起動まで最低でも 4RTT 掛かっているのでほとんど猶予がない DOM に介入することで LCP に関与する可能性が高い とにかく頑張る

Slide 31

Slide 31 text

UI コンポーネント配信 : 大まかな流れ

Slide 32

Slide 32 text

UI コンポーネント展開までのクリティカルパス

Slide 33

Slide 33 text

UI ライブラリの選定 ビジュアルとコードの双方向編集のために AST が必要 ランタイム面で React や Vue は厳しい [email protected] : 130k [email protected] : 97.1k コアが軽量なものから選ぶ preact : 11k svelte : 6.7k lit : 16k

Slide 34

Slide 34 text

UI ライブラリの選定 : Preact React 風 API のライブラリ React と基本同じだが、React 資産と混ぜられるわけではない SSR 周りが React と別路線 ( というかReact|Next が特殊すぎる) /** @jsx h */ import {h} from "preact"; import {useState} from "preact/hooks"; function Counter() { const [value, setValue] = useState(0); return setValue(value + 1)}>{value}; }

Slide 35

Slide 35 text

UI ライブラリの選定 : Svelte ビルド時最適化に寄せたUI ライブラリ .svelte のテンプレートからコードを生成 export let name: string;
{name}
.red { color: red; }

Slide 36

Slide 36 text

UI ライブラリの選定 : Lit(-Html) WebComponents API に近い命令セットをもつ軽量ランタイム Tagged Template Literal で宣言的なテンプレートを記述する import {html, css, LitElement} from 'lit'; import {customElement, property} from 'lit/decorators.js'; @customElement('simple-greeting') export class SimpleGreeting extends LitElement { static styles = css`p { color: blue }`; @property() name = 'Somebody'; render() { return html`

Hello, ${this.name}!

`; } }

Slide 37

Slide 37 text

検討結果 : Svelte の採用 ランタイム svelte/internal が非常に小さい (6.7k) rollup の作者だけあってビジュアルエディタを作るための静的解析 ツールが揃っている 動的要素を使わないテンプレートが素の HTML/CSS に近い JS に詳しくなくとも心理的抵抗が少ない Scoped CSS と shadowRoot オプションがある

Slide 38

Slide 38 text

ビジュアルエディタの設計を考える ローカルプレビュー rollup + svelte をブラウザに埋め込んでコンパイル 双方向編集 ソースコードをマスターデータとする 特定の AST のパターンを満たす場合、フォームに変換する KARTE 公式に提供可能するものはこのパターンを満たす 直接編集でパターンを崩した場合、直接編集のみ可

Slide 39

Slide 39 text

ビジュアルエディタ : レイアウトとエレメント レイアウト : CSS Grid Grid 構造を素朴なデータ構造に変換しエディタのフォームに変換 CSS Flex では縦横の切り替えに入れ子が必要になり複雑 Grid 要素にコンポーネントを割り当てる 割り当てられたComponent の Attibute に対する操作をエディタ で実装 https://zenn.dev/mizchi/articles/programmable-grid

Slide 40

Slide 40 text

ビジュアルエディタの生成コード // 組み込みコンポーネント。未使用のものは DCE で消える。 import { Grid, GridArea, ImageElement, TextElement } from "./components"; ... コード自体に静的解析によるビルド最適化を織り込んでおく

Slide 41

Slide 41 text

ビジュアルエディタ : プロトタイピングと実装 プロトタイピング AST による双方向操作の実現の検証 リアルタイムプレビューの実装の検証 グリッド操作のデータ構造とロジックの実装 ビジュアルエディタ自体のUI 周りは作業量が多いので別チームに任 せることにした ※自分はビルドパイプラインの構築とスクリプト部分に注力

Slide 42

Slide 42 text

余談 : Qwik の紹介 SSR ファーストなライブラリ JSX + 独自API セット 理論上は最小のコードを出力できる 初回リクエストにHTML だけ返しつつJS 配信を遅延 onclick や onhover で初めて JS ロジックを注入して発火

Slide 43

Slide 43 text

余談 : Qwik のコード例 import { component$, useStore } from '@builder.io/qwik'; export default component$(() => { return ( <>
alert('Hello')}>greet!
> ); });

Slide 44

Slide 44 text

余談 : Qwik の出力例 click されるまでイベントハンドラの登録しか行われない Astro のアイランドアーキテクチャも似たようなもの 3rd party scirpt なら検討の価値がある

Slide 45

Slide 45 text

3rd party script 実装編

Slide 46

Slide 46 text

コア部分の実装ポリシー ライブラリは使わない IE サポートしない分、 ES2015+ が使える 常に計測する 静的検査+ ユニットテスト+E2E のコスパを考慮 いわゆるテストピラミッド

Slide 47

Slide 47 text

ライブラリの使用 (npm 等 ) 使わない 使った瞬間細かい最適化は全部吹き飛ぶ よく使われるライブラリは過度に一般化されている 必要なものは自分で作る comlink 代替 https://github.com/mizchi/minlink zod 代替 https://github.com/mizchi/lizod ↑ も結局使ってない( 必要な部分だけ組み込んだ)

Slide 48

Slide 48 text

ユニットテスト ライブラリを使わない分コードが増えるのでユニットテストを頑張 る cookie や DOM の特殊な挙動は E2E で書く vitest を採用( 特に不満はない)

Slide 49

Slide 49 text

E2E Test playwright でクロスブラウザテスト MS 製の E2E テストランナー 雑な wait を書かずに waitFor で制御できれば実行が速い Safari 環境は playwright の webkit で代用 E2E には素直に専用の @playwright/test を使った方がいい flaky tests の再実行や snapshot の組み込み等が便利 アサーションの expect() が独自なのがちょっと残念

Slide 50

Slide 50 text

他、実装時の問題 userAgent をパースしていたが、UA parser のライブラリが重い 標準的なエラートラッカーが使えない

Slide 51

Slide 51 text

UA Paser 有名所は https://github.com/faisalman/ua-parser-js だが... サイズがでかい(16.4k) 現代では UA による分岐はそもそも必要ないのでサーバーで解析

Slide 52

Slide 52 text

エラートラッカー sentry や bugsnag のクライアントが重い @sentry/browser : 267.3kB @bugsnag/js : 43.5kB そもそも3rd party なので window.onerror を全部収集されても困る 自前で try-catch して error.stack を文字列としてサーバーに送信 stcaktrace-js でサーバー側で元エラーを復元 https://www.npmjs.com/package/stacktrace-js

Slide 53

Slide 53 text

ビルドサイズの計測 今回は https://www.npmjs.com/package/rollup-plugin-analyzer CI でも超えちゃいけないラインを設定( およそ現在値+10% を指定) 仮説と検証を繰り返して、本当に小さくなるか確認 https://try.terser.org/ は友達 困ったら目grep する

Slide 54

Slide 54 text

目 grep しよう ! 意味は追わず模様として見る 同じ模様( 文字列) が何度も出現していることがわかる

Slide 55

Slide 55 text

Lighthouse の計測 Core Web Vitals の計測 GitHub Actions 週次実行などして slack に貼る CI でやるにはくどいかも CLI でもいい https://github.com/GoogleChrome/lighthouse-ci

Slide 56

Slide 56 text

3rd party script 実装 : 結果 次に解説するビルド時最適化と合わせて 25kb ( 最小設定) を達成 時間経過で膨らむので、社内の啓蒙が大事(CI も大事)

Slide 57

Slide 57 text

3rd party script 最適化編

Slide 58

Slide 58 text

最適化の前に 設計の失敗は小手先のビルドオプションでは挽回できない 30kb を 20kb に削る技術なので最後のフェーズ 3rd party scirpt は( 本来) そこまでやらねばならぬ

Slide 59

Slide 59 text

TypeScript の transpile 出力コードを減らすには target を可能な限り上げる tslib の使用を検討する minifier(terser) が知り得ない情報を減らす

Slide 60

Slide 60 text

TypeScript ビルドターゲット TypeScript で async/await を変換するとコードが膨らむ es5 の class 変換も同上 今回は es2017 に設定

Slide 61

Slide 61 text

typescript: tslib tsconfig.json を importHelpers: true にすると TS が生成するヘル パを tslib から解決するようになる 何度も似たようなコードを展開しているときに有用

Slide 62

Slide 62 text

typescript: tslib の実例 export async function request() { const res = await fetch("/get"); return res.text(); } importHelpers: true, target: 'es2015' import { __awaiter } from "tslib"; export function request() { return __awaiter(this, void 0, void 0, function* () { const res = yield fetch("/get"); return res.text(); }); }

Slide 63

Slide 63 text

tslib: __awaiter の中身 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; tslib ではない場合、async/await を使うファイルごとに展開される

Slide 64

Slide 64 text

tslib は有用か ES2015 で async/await を都度展開しているときには効く esbuild や swc はサポートしていない 自分は使わなかった

Slide 65

Slide 65 text

ビルド時最適化

Slide 66

Slide 66 text

ビルド最適化の基本 minify: 意味論的に同じコードに変換 treeshake: ESM で未参照コードを削除 DCE: 不要コード削除。 定数置換後の if(false) 等 mangle: 結果に影響しない変数をリネーム

Slide 67

Slide 67 text

不要コード削除のコツ 定数置換後にこの状態を作ることを目標とする import {a, b} from "./feat"; if (false) a(); if (true) b();

Slide 68

Slide 68 text

terser の不得意を知る 明示的に与えた情報にはめっぽう強い mangle.properties 数文字縮める程度の細かいテクニックが多い undefiend => void 0 TS のような静的解析はない 深いプロパティアクセスに弱い this やクラス関連に弱い

Slide 69

Slide 69 text

terser: プロパティアクセスに弱い obj.foo は getter で副作用が起きる可能性がある const obj = { cnt: 0, get foo() { this.cnt++; return cnt; } } 実行回数を指定する passes で複数実行すると展開されることがあ るが、制御は困難

Slide 70

Slide 70 text

BAD パターン : オブジェクト // BAD: 参照にプロパティアクセスを経由するので Treeshake が効かない export const Constants = { FOO: 1, BAR: 2, }; // BAD: defalut を経由するので同上 export default { BAZ: 3, QUX: 4 }; // BAD: TS が冗長なオブジェクトに展開するのでバンドルサイズに悪い export enum MyEnum { XXX, YYY }

Slide 71

Slide 71 text

GOOD パターン : オブジェクト // GOOD: 個別に import できるので treeshake 可能 export const FOO = 1; export const BAR = 2; export const BAZ = 3; export const QUX = 4; // const enum はビルド時に定数置換される // ただし MyEnum[MyEnum.XXX] で元キー名を取得することができない export const enum MyEnum { XXX, YYY } 基本、定数は const で直接宣言する 型レベルの READONLY 属性は terser には伝わらない

Slide 72

Slide 72 text

terser: Class の最適化は辛い export class Foo { #hard: number = 1; public foo() { return this.#hard } private bar() {} } // 展開後: target: es2021 , importHelpers: true var _Foo_hard; import { __classPrivateFieldGet } from "tslib"; export class Foo { constructor() { _Foo_hard.set(this, 1); } foo() { return __classPrivateFieldGet(this, _Foo_hard, "f"); } bar() { } } _Foo_hard = new WeakMap();

Slide 73

Slide 73 text

Class の最適化しづらさ soft private は型レベルでアクセスを禁じているだけで terser には 伝わらない terser レベルでは、未使用プライベートスコープの解析ができない ES2022 未満だと hard private のポリフィルが必要

Slide 74

Slide 74 text

クラス最適化 : 内部アクセスパターン export class Foo { public getValue() { return new Internal().getInternalValue(); } } class Internal { // 他クラスからアクセスされるので public だがモジュール外からアクセスされない public getInternalValue() { return { internal: 1 }; } } よくある内部クラス Rust の pub(crate) fn func() {...} 相当がほしいね...

Slide 75

Slide 75 text

クラス最適化 : 内部アクセスパターン terser で mangle.properties.regex: /^(_|\$)/ を設定 export class Foo { public getValue() { return new Internal().$getInternalValue(); } } class Internal { public $getInternalValue() { return { $internal: 1 }; } } // minify 後: `$` ではじまるものは mangle される export class Foo{getValue(){return(new e).t()}}class e{t(){return{l:1}}}

Slide 76

Slide 76 text

terser のルールづくり モジュール内 public は $foo , クラス内部プロパティは _foo とする そもそも自分が公開API に $.* を使ってないことを保証する必要 { mangle: { properties: { regex: /^(__|\$)/ } } }

Slide 77

Slide 77 text

余談 : JS に class は必要 ? 個人的には全く不要 クラスベースの別の言語から移行してくる人の受け皿でしかない フロントエンドはJSON にシリアライズする頻度が高いのでJSON サ ブセットの型+ 関数で十分 type MyData = { foo: number; bar: string; } export function createMyData(foo: number, bar: string) { return {foo, bar} }

Slide 78

Slide 78 text

型レベル最適化の試み

Slide 79

Slide 79 text

3rd party の開発を通して感じたこと terser が型情報を見ないのがもったいない 現代なら TypeScript の型情報で最適化することが可能なはず... このノウハウを一般化して還元できないか?

Slide 80

Slide 80 text

実験 1: クラスを分解するコンバータ export class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; console.log("Point created", x, y); } distance(other: Point) { return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)); } } こういうコードを分解するための TS Transformer を書いてみた

Slide 81

Slide 81 text

実験 : クラスを分解するコンバータ export type Point = { x: number; y: number; }; export function Point$new(x: number, y: number): Point { const self: Point = { x: x, y: y }; console.log("Point created", x, y); return self; } export function Point$distance(self: Point, other: Point) { return Math.sqrt(Math.pow(self.x - other.x, 2) + Math.pow(self.y - other.y, 2)); } $ npm install @mizchi/declass $ npx declass input.ts # -o output.ts

Slide 82

Slide 82 text

実験 1: クラスを分解するコンバータの考察 完全な変換は難しいので例示する用にしかならない 継承が絡むと完全な変換はできない ぶっちゃけ簡単なケースなら chatgpt にやらせれば良さそう

Slide 83

Slide 83 text

実験 2: dts-analazyer TypeScript の .d.ts に出現するキーを公開API として、 terser の予 約語として使えないか? mangle.properties.regex: /.*/ ( 要は全部) と mangle.properties.reserved の明示的な mangle 回避を組み合わ せる やってみた

Slide 84

Slide 84 text

https://zenn.dev/mizchi/articles/optools-minify-the-world

Slide 85

Slide 85 text

実験 2: dts 解析の結果 ESM インターフェースの範囲では安全だが、内部副作用の型も必要 ビルドに含められない external な import への引数 環境ビルトインへの操作 (window, Node のシステムコール等) fetch() , Worker.postMessage , workerThreads の外部通信 内部副作用型はエントリポイントで export type ... の運用でカバ ーできるが...

Slide 86

Slide 86 text

副作用 - 外界との接続を考える

Slide 87

Slide 87 text

実験 3: 型レベル minify 内部副作用も考えて、結局型レベルの解析機に取り組むことにした TypeScript LanguageService (IDE との対話プロトコル) を使えば...!

Slide 88

Slide 88 text

実験 3: Packelyze Transformer https://github.com/mizchi/packelyze/tree/main/transformer TypeScript の LanguageService(IDE との対話API) を使って、型レベ ルで解析する TS から TS に変換する中間トランスフォーマー vite(rollup) plugin を想定

Slide 89

Slide 89 text

実験 3: Packelyze Transformer のアプローチ ビルド時のエントリポイントに関与する型シンボルを列挙 export function foo(input: Input): Output {...}; 副作用を起こす API に関与する型シンボルを列挙 fetch({ body: JSON.stringify({/* here */}) }) GlobalVar.xxx = {/* here! */} ; 外界と関わらないインターフェースを全部 mangle

Slide 90

Slide 90 text

Demo ( 時間があったら)

Slide 91

Slide 91 text

実験 3 型レベル解析 - 進捗 単純に難しい! TypeScript Compiler API に詳しくなる https://zenn.dev/mizchi/articles/typescript-code-reading トップレベル以外の export されたシンボル解析に苦戦中 そもそも TS の rename 処理は型安全ではない 機能を絞れば、もうちょっとでリリースできる( かも)

Slide 92

Slide 92 text

最後に 3rd party script のパフォーマンスバジェットは限られている 設計レベルの失敗はビルド最適化で巻き返せない 現代の minifier は型追跡を持たないので、TS 時点で minify 想定の コードを書かないといけない ↑ の問題を解決するコンパイラを作ろうとしている(WIP)

Slide 93

Slide 93 text

おわり