Slide 1

Slide 1 text

Web Streams APIの 基本と実践、 TypeScriptでの活用法 2025.05.24 TSKaigi 2025 @tasshi_me 1

Slide 2

Slide 2 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages はじめに 田代 雅治 (@tasshi_me) • 仕事 • サイボウズ株式会社 • kintone開発 拡張基盤 チーム EM • DX (Developer eXperience) デザイン • 週4勤務、週1で副業 • 主にnpmパッケージとサーバーサイドJS • (画面はあまり触らない) • 去年から会社の同期とバンド始めました 自己紹介 2

Slide 3

Slide 3 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages はじめに • kintone ≒ かんたんに業務アプリを構築できるクラウドサービス • kintoneのプラグイン・連携サービス向けの開発基盤を開発・保守するチーム • API開発、SDK/CLIなどのOSS提供 • npmパッケージを多数メンテナンス kintone開発 拡張基盤 チーム 3

Slide 4

Slide 4 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages はじめに Streams APIを触ったことがないという声を聞くので、ざっくりStreams APIの説明をします。 また、Node Streamとの違いや、Promiseベースの非同期処理との相互運用性についても話します。 Streams APIを試すきっかけとなると良いかなと思います。 そして、今後の実装の選択肢にStreams APIが追加されたらとても嬉しいです。 (TSKaigiですが、型の話は少ないかも) このセッションでは 4

Slide 5

Slide 5 text

ストリームとは何か 5 ※様々な技術領域・開発言語でストリームの定義があるため ここでは概ね共通と思われる性質について話します

Slide 6

Slide 6 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages ストリームとは何か • データ入出力を逐次的に、効率よく扱うためのデータ構造 • データをより細かい分割された単位の連続した流れとして表現する • 昔からある概念 • 多くの開発言語でストリーム操作のインターフェースは提供されている • 古くはUNIXの標準ストリーム、pipe、redirectとか ストリームとは何か 6 変換処理A chunk chunk chunk 変換処理B chunk chunk chunk 変換処理C chunk chunk chunk chunk

Slide 7

Slide 7 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages ストリームとは何か 標準ストリームとパイプの例 7 cat input .txt sed “foo” tee Terminal output .txt “bar” “bar” “bar” “foo” stdout stdin stdin stdout stdout

Slide 8

Slide 8 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages ストリームとは何か • メモリ空間を圧迫しにくい • データを処理する分だけメモリ上に展開するため • 大規模データや時間経過で無限に増大するデータの処理に有効 • 低遅延 • 先頭のデータが処理されるまでの時間が速い • ※最終的にデータ全体が処理されるまでの時間が早くなるとは限らない ストリームのメリット 8 処理A 処理B ストリーム処理の場合 バッチ処理の場合 時間経過 時間経過

Slide 9

Slide 9 text

Web Streams API 9 WHATWGのStreams API (https://streams.spec.whatwg.org/)

Slide 10

Slide 10 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API JavaScriptでストリーム処理を行うためのAPI • データは分割された断片(chunk)の連続した流れとして扱われる • 3種類の役割の異なるストリームオブジェクト • ストリームオブジェクト同士をパイプ接続(パイプチェーン)することで、 chunkは流れるように終端まで処理される Streams APIの概要 10 Transform Stream Readable Stream Writable Stream chunk データの読み込み データの変換 (読み込み+書き込み) データの書き込み chunk Source Sink

Slide 11

Slide 11 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API 具体例: WebリソースをFetchしてファイルに保存 11 Text Decoder Stream Response. body FileSystem Writable FileStream string Uint8 Array example .com File System

Slide 12

Slide 12 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API 基となるソース(underlying source)から流れるデータを表現するオブジェクト ソースから流れるデータをチャンクに分割し、ストリーム処理できる形で提供する • 基となるソース:ファイルシステム、ネットワークリソース、メモリ上の配列、など • Pull型/Push型のソースがある(付録を参照) • ReadableStreamの例:Fetch APIのResponse.body、Blob.stream() ReadableStream (読み取り可能なストリーム) 12 Readable Stream chunk Source chunk 読み出し raw data

Slide 13

Slide 13 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API ストリームに流れるデータをある形式から別の形式に変換するオブジェクト • TransformStreamの例 • TextEncoderStream / TextDecoderStream: バイナリ 文字列の変換 • CompressionStream / DecompressionStream: データの圧縮・展開 (gzip, deflate) TransformStream (変換ストリーム) 13 chunk (string) chunk (Uint8Array) chunk (Uint8Array) TransformStream chunk (string)

Slide 14

Slide 14 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API 基となるシンク(underlying sink)に流れるデータを表現するオブジェクト • 基となるシンク:ファイルシステム、データベース、など • WritableStreamの例:File System Access APIのFileSystemWritableFileStream WritableStream (書き込み可能なストリーム) 14 Writable Stream chunk Sink chunk 書き込み data

Slide 15

Slide 15 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API • ReadableStream、TransformStream、 WritableStreamを継承して独自のストリームを作成する • データ処理用のいくつかのメソッドを実装する(pull, write, transform, etc.) • チャンクの型はGenericsで指定 • 後述のキューイング戦略もコンストラクタで指定 • DBアクセスとか、データエンコーディングとか、独自の処理を実装できる 独自のストリームオブジェクトを作成する 15

Slide 16

Slide 16 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API • Stream同士をパイプ接続すると、chunkは流れるように終端まで処理される(パイプチェーン) • パイプ接続メソッド • Readable.pipeTo(): 終端のWritableStreamに接続 • Readable.pipeThrough(): 中間のTransformStreamに接続 パイプチェーン 16 Transform Stream Readable Stream Writable Stream chunk chunk Source Sink pipeThrough() pipeTo()

Slide 17

Slide 17 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Streams API • 1つのReadableStreamを2つのReadableStream(branch)に分配 • 分配したストリームはそれぞれ異なる速度で読み取ることができる • FetchしたデータをUIとキャッシュの両方に出力したりできる • 注:内部キューにチャンクが滞留するため、長すぎるデータストリームには適さない(付録を参照) ストリームの分配 (tee) 17 Readable Stream 元のストリーム Readable Stream 分配後のストリーム1 Source Readable Stream 分配後のストリーム2

Slide 18

Slide 18 text

利用イメージ 18 1. 他サービスからのデータインポート 2. GPTの回答をリアルタイム表示

Slide 19

Slide 19 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 利用イメージ • メモリ負荷を軽減できる例 • 他サービスからAPI経由で全レコードを取得し、加工して自サービスのDBに書き込んでいく • APIのレスポンスはJSONL形式 1. 他サービスからのデータインポート 19 Text Decoder Stream Response JSON Parser Stream Line Splitter Stream 行ごとに 再分割 (Transform) UTF-8 デコード (Tranform) JSONから オブジェクトに変換 (Transform) レスポンスの 読み込み (Readable) Writable Stream DBに 書き込み (Writable) fetch DB

Slide 20

Slide 20 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 利用イメージ • リアルタイム性を実現できる例 • API経由でGPTの回答を取得し、画面に表示する • デフォルトの一括応答では、回答全体が生成されてからレスポンスが返却される • ストリーミング応答を有効化すると、回答が生成された分ずつ返却されるようになる 2. GPTの回答をリアルタイム表示 20

Slide 21

Slide 21 text

内部キューと背圧 21 パイプチェーンの処理速度を調整する仕組みについて

Slide 22

Slide 22 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 内部キューと背圧 • ストリーム(オブジェクト)は未処理チャンクを保持する内部キューを持っている • パイプチェーン上のストリーム間で処理速度に差がある場合、 内部キューにチャンクが溜まっていくことになる => メモリを圧迫する? ストリームオブジェクト間の処理速度の違いとメモリ使用量 22 Transform Stream Readable Stream Writable Stream chunk chunk チャンクが溜まっていく? ここの処理が遅い場合 (DBアクセスなど) ※TransformStream は 書込側・読出側それぞれに 内部キューを持つが省略

Slide 23

Slide 23 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 内部キューと背圧 • チャンクを受け入れられない場合、ストリームは上流に停止信号を出す • 停止信号を受けた上流のストリームはデータの送信を停止する • 下流のストリームが送信を指示(pull)すると再び処理が再開する => チャンクの流速が調整されてメモリを圧迫せずに処理できる 背圧 (Backpressure) 23 Transform Stream Readable Stream Writable Stream chunk chunk 内部キューが満杯 STOP! 内部キューが満杯 STOP!

Slide 24

Slide 24 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 内部キューと背圧 • 背圧は内部キューの状態から、キューイング戦略に基づいて通知される • 現在は2種類のキューイング戦略が利用可能 キューイング戦略と最高水準点(highWaterMark) 24 キューに格納されたチャンク数で判定 キューに格納されたバイト数で判定 chunk chunk chunk chunk chunk chunk chunk chunk 3 最高水準点 (highWaterMark) 10KB CountQueuingStrategy ByteLengthQueuingStrategy

Slide 25

Slide 25 text

Node.jsのStreamとの違いと互換性 25 https://nodejs.org/api/stream.html

Slide 26

Slide 26 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Node.jsのStreamとの違いと互換性 • Node.jsの組み込みモジュール • こちらのほうが先発 • 「Streamを制するものはNode.jsを制す」と言われていたらしい • EventEmitterを継承していて、イベント駆動的 • highWaterMarkの考え方はNode StreamからWeb Streams APIに影響してそう • 3(+2)種類のストリームオブジェクト Node.js Streamについて 26 Readable 読み込み Duplex 双方向 (読み込み+書き込み) Transform 変換 Writable 書き込み PassThrou gh パススルー (何もしない)

Slide 27

Slide 27 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Node.jsのStreamとの違いと互換性 • Node.jsでもv21でWeb Streams APIがStableになった • Node Stream Web Streams API は toWeb() / fromWeb() メソッドで相互に変換可能 • v17で追加されてから長らくExperimentalだったが、v24でとうとうStableになった Node SteamとWeb Streams APIとの互換性 27

Slide 28

Slide 28 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Node.jsのStreamとの違いと互換性 • Web標準であること • クロスブラウザに利用できる • Fetch APIを始めとして、他の標準仕様にもWeb Streams APIベースのAPIが増えていっている • WinterTC Minimum Common APIにも含まれている • Node.js含めてサーバーサイドJSでも利用できる • インターフェースの改善 • EventEmitter/Callbackな書き方からPromise/async/awaitな書き方に • 利用者に公開されているメソッド・プロパティがかなり減っているため学習コストが減った • (逆に細かい制御がしづらくなったとも) • 型情報 (@types/node) • Node Stream: chunkの型がany • Web Streams API: chunkの型がGenericsで指定できる Node Streamと比べて良くなったと思うところ (1) 28

Slide 29

Slide 29 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Node.jsのStreamとの違いと互換性 • エラーハンドリング • Node Streamでは、上流のストリームがエラー終了しても下流のストリームが閉じない • errorイベントのイベントリスナーで明示的に閉じる必要がある • Web Streams APIでは、勝手に閉じる • pipeThrough()/pipeTo()のpreventAbortオプションで制御可能 Node Streamと比べて良くなったと思うところ (2) 29

Slide 30

Slide 30 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Node.jsのStreamとの違いと互換性 • 変換ストリームの結合が比較的やりやすくなった • Node Stream: Duplexでラップするが、実装が複雑になる • イベントリスナーやメソッドの繋ぎ込みがかなり面倒 • stream.compose()を利用すると簡単に結合できるが、まだExperimental • Web Streams API: TransformStreamでラップしてパイプ接続したらOK Node Streamと比べて良くなったと思うところ (3) 30

Slide 31

Slide 31 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Node.jsのStreamとの違いと互換性 • これから書くコードは最初からWeb Streams APIで良い • Web標準かつPromiseベースの書き方ができる • Node Streamとも toWeb() / fromWeb() で相互変換できる • 昔からあるnpmパッケージはNode Streamを使っている • 当面は toWeb() / fromWeb() メソッドで変換しながら併用することになる Node SteamとWeb Streams API、どっちを使えばいい? 31

Slide 32

Slide 32 text

Promiseベースの非同期処理との相互運用性 32 • ストリームとPromise • ストリームと反復処理

Slide 33

Slide 33 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Promiseベースの非同期処理との相互運用性 • pipeTo()の返り値がPromise • パイプチェーンでのデータ処理が終わるまでawaitで待つことができる • チャンクを1つずつ操作したい場合 • ReadableStream.getReader()やWritableStream.getWriter()でreader/writerを取得 • await reader.read() / await writer.write() でチャンクを1つずつ読み込み/書き込みできる ストリームをasync/awaitの中で使う 33

Slide 34

Slide 34 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Promiseベースの非同期処理との相互運用性 • ReadableStreamは非同期反復可能([Symbol.asyncIterator]()を実装している)(Safari除く) • for await ... ofで反復処理できる • Array.fromAsync()でデータを全て読み出して配列に格納できる ストリームを反復処理する 34

Slide 35

Slide 35 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages Promiseベースの非同期処理との相互運用性 • ReadableStream.from() • 反復可能オブジェクト or 非同期反復可能オブジェクトからReadableStreamを作成できる • ただしまだFireFox, Deno, Node.jsでしか利用できない イテラブルからストリームを作成する 35 https://developer.mozilla.org/ja/docs/Web/API/ReadableStream/from_static

Slide 36

Slide 36 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages おわりに • Streams APIには3つのストリームオブジェクトがある • ReadableStream, TransformStream, WritableStream • ストリームオブジェクトをパイプ接続してデータを逐次処理できる • 背圧を制御することで処理速度を制御し、メモリ使用量を適切に抑えることができる • Node Streamとは相互に変換可能 • 新規に書くコードはWeb Streams APIで良い • async/awaitな処理とも組み合わせやすい まとめ 36

Slide 37

Slide 37 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages おわりに • kintoneを開発する仲間を探しています!! • 採用フォームからでも、XのDMでもどうぞ! • https://cybozu.co.jp/recruit/ We are hiring!! 37 Webエンジニア (kintone) エンジニアリング マネージャー (kintone) Webエンジニア (kintone/生成AI) フロントエンド エキスパート

Slide 38

Slide 38 text

ご清聴ありがとうございました 38 Ask The Speakerやります! お気軽にご質問ください! 場所:3F 企画AREA 時間:13:50-14:00

Slide 39

Slide 39 text

付録 39 • ストリームオブジェクトの補足 • ストリームの分配 (tee) と背圧制御の問題 • ネットワーク通信に背圧は反映されているのか?

Slide 40

Slide 40 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 付録:ストリームオブジェクトの補足 • チャンクの読み込みはReader経由で行う • ソースにはPull型とPush型がある • Pull型:データはストリームから明示的に読み込む • 例:ファイルアクセスなど • https://streams.spec.whatwg.org/#example-rs-pull • Push型:データは勝手にソースから送信される、イベントリスナなどでストリームにenqueueする • 例:動画ストリーム、TCP/WebSocketsなど • https://streams.spec.whatwg.org/#example-rs-push-backpressure ReadableStream 40 Source chunk enqueue ReadableStream chunk chunk chunk chunk Reader 内部キュー raw data

Slide 41

Slide 41 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 付録:ストリームオブジェクトの補足 • チャンクの書き込みはWriter経由で行う WritableStream 41 Sink chunk enqueue WritableStream chunk chunk chunk chunk Writer 内部キュー data

Slide 42

Slide 42 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 付録:ストリームオブジェクトの補足 • 内部的にはWritableStream + ReadableStream • 入力側: TransformStream.writable (WritableStream) • 出力側: TransformStream.readable (ReadableStream) • 内部キューも入力側・出力側それぞれにある TransformStream (変換ストリーム) 42 readable chunk chunk writable chunk TransformStream transform() chunk データの入力側 データの出力側 データの変換

Slide 43

Slide 43 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 付録 • tee()で分配された2つのReadableStreamは消費速度が速い方の速度で背圧制御される • 時間経過と共に速度が遅い方の内部キューにデータが滞留してしまう • 背圧制御を変更するオプションが提案されている(whatwg/streams#1235) • Cloudflare Workersでは tee の背圧制御を独自に修正している(cloudflare/workerd#85) • https://community.cloudflare.com/t/467416 ストリームの分配 (tee) と背圧制御の問題 43 Readable Stream Readable Stream 両方に同時にchunkを送信 後続の 処理が速い 後続の 処理が遅い ここにチャンクが溜まってしまう

Slide 44

Slide 44 text

Web Streams APIの基本と実践、TypeScriptでの活用法 – TSKaigi 2025 #TSKaigi #TSKaigi2025 #tskaigi_leverages 付録 • 検証時の通信方式 • APIレスポンスはJSONL形式 (Content-Type: application/jsonl; charset=utf-8) • HTTP/1.1 • Transfer-Encoding: chunked (レスポンスはチャンク単位で送られてくる) • => 背圧はunderlying sourceへのネットワークリクエストにも反映される • 内部キューが溜まってくるとウィンドウサイズが小さくなり、受信可能データを調整する • それでも処理しきれずに受信を止める場合は、zero windowパケットが送信される ネットワーク通信に背圧は反映されているのか? 44 chunk Server chunk chunk Response STOP! STOP! (zero window) https://speakerdeck.com/tasshi/web-streams-api-and-tcp-flow-control