JavascriptWorkerによる処理の外部委託化
onmessage
postMessage
terminte
Worker
マルチスレッド
非同期
元々、Javascriptでは仕様上シングルスレッドという形での処理しかできない状態でしたが、
Worker という技術の登場で マルチスレッド を実現できるようになりました。
初出がいつかはわからないですが、w3cではWDとして2009年が初出?のようでした。
Worker にはいくつかの種類がありますが、
ベースになる Web Worker に関して解説していきます。
Worker とは
MDN にはこのように記載されています。
Javascriptの処理はページが表示される際に、
読み込み→実行の段階で HTML, CSS の読み込みを阻害してしまうブロッキングの状態が発生してしまいます。
これは、Javascriptの処理時間が長ければ長いほどブロックする時間が長くなります。
ここで登場するのが今回の Worker になります。
この長く時間がかかってしまうスクリプト処理を Worker に預けることで、
メインスレッド上で行うことがなくなり、ブロッキングの軽減につながります。
例えるならば、
自分がたくさんの仕事を抱えていると仮定してください。
同時並行で仕事を片付けている(HTML、cssを処理している)最中に、
急に最優先で行わなければいけない仕事(Javascriptの実行)が発生してしまいます。
差し込みの仕事(Javascript)が入ったことで、元々の仕事(HTML、css)を中断しなければならなくなりました。
差し込みの仕事はそこまで難しい内容ではないけれども単純に時間がかかります。
「この時間がかかる差し込みの仕事を誰かにお願いできたらなぁ」を実現する内容になります。
Worker の種類
Workerには何個か種類がありそれぞれに特徴があります。
1, 専用ワーカー:Worker
2, 共用ワーカー:SharedWorker
3, サービスワーカー:ServiceWorker
の3種類になります。(2023/03現時点)
今回、解説するWorkerは1番の専用ワーカーになりますが、
他のワーカーについて簡単に紹介だけしておきたいと思います。
共用ワーカー:SharedWorker
同じドメインの場合に限ってですが、別ウインド(別タブ)もしくはiframeとのデータがやりとりできる仕組みを提供してくれます。
イメージとしては、
オフィスという括りの中で、各それぞれのPC(別ウインド・タブ、iframe)が、
共用のデータストレージにアクセスしてデータのやり取りが可能になる状態だと思っていただければ、
分かりやすいのではないかと思います、
サービスワーカー:ServiceWorker
PWA(Progressive Web Application)で耳にするようになった機能で、
プロキシサーバーとして動いたり、キャッシュストレージとして動かしたり、
PUSH通知を受け取る窓口だったりを担う機能になります。
できること、動作させることができる内容が多いため一概にこれができるといったことが言えません。
専用ワーカー、共用ワーカーとは立ち上げ方が違う点も要注意です。
Worker の特徴
Workerはメインスレッドのようになんでも処理できるわけではなく、
計算等のような単純な処理に特化しています。
実際、メインスレッドでの処理とWorkerによる処理で、1億までプラス1づつカウントを行った場合、
Workeでの処理の方が早く完了します。
逆に出来ないことは、DOM(HTML要素)の操作ができません。
文字列の操作は可能なので、テンプレートをWorkerに渡して加工したのち、メインスレッドでDOM(HTML要素)に変換することは可能です。
Worker 内で使用できるブラウザAPI
- Broadcast Channel API
- Cache API
- Channel Messaging API
- Console API
- Crypto
- CustomEvent
- DOMRequest
- DOMCursor
- Fetch
- FileReader
- FileReaderSync
- FormData
- ImageData
- IndexedDB
- Performance
- PerformanceEntry
- PerformanceMeasure
- PerformanceMark
- PerformanceObserver
- PerformanceResourceTiming
- Promise
- ServiceWorkerRegistration
- TextEncoder
- TextDecoder
- URL
- OffscreenCanvas
- WebSocket
- XMLHttpRequest
- responseXML
- channel
Worker 内で使用できるメソッド
利用できるものはJavascriptリファレンスに記載されているものは使用できるので、下記リンクより確認ください
MDN JavaScript リファレンス
上記に加えて下記も使用できます。
importScripts によって外部スクリプトの読み込みが可能になります。
postMessage によってWorker内で処理した結果をブラウザ側に戻すことができます。
Worker 内で使用できないオブジェクト
まず、Window(window) が使用できません。
前にも出ましたが、HTMLに関する操作が一切できません。
Worker 内で実際に使用できるメソッド一覧(Chrome v111)
数が多いので興味がある方は下記のより確認ください
DedicatedWorkerGlobalScope で使用できる全メソッド一覧
- Object
- Function
- Array
- Number
- parseFloat
- parseInt
- Infinity
- NaN
- undefined
- Boolean
- String
- Symbol
- Date
- Promise
- RegExp
- Error
- AggregateError
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
- globalThis
- JSON
- Math
- Intl
- ArrayBuffer
- Uint8Array
- Int8Array
- Uint16Array
- Int16Array
- Uint32Array
- Int32Array
- Float32Array
- Float64Array
- Uint8ClampedArray
- BigUint64Array
- BigInt64Array
- DataView
- Map
- BigInt
- Set
- WeakMap
- WeakSet
- Proxy
- Reflect
- FinalizationRegistry
- WeakRef
- decodeURI
- decodeURIComponent
- encodeURI
- encodeURIComponent
- escape
- unescape
- eval
- isFinite
- isNaN
- console
- PushSubscriptionOptions
- PushSubscription
- PushManager
- Permissions
- PermissionStatus
- PeriodicSyncManager
- Notification
- NavigatorUAData
- MediaSourceHandle
- CropTarget
- BackgroundFetchRegistration
- BackgroundFetchRecord
- BackgroundFetchManager
- XMLHttpRequestUpload
- XMLHttpRequestEventTarget
- XMLHttpRequest
- WritableStreamDefaultWriter
- WritableStreamDefaultController
- WritableStream
- WorkerNavigator
- WorkerLocation
- WorkerGlobalScope
- Worker
- WebSocket
- WebGLVertexArrayObject
- WebGLUniformLocation
- WebGLTransformFeedback
- WebGLTexture
- WebGLSync
- WebGLShaderPrecisionFormat
- WebGLShader
- WebGLSampler
- WebGLRenderingContext
- WebGLRenderbuffer
- WebGLQuery
- WebGLProgram
- WebGLFramebuffer
- WebGLBuffer
- WebGLActiveInfo
- WebGL2RenderingContext
- UserActivation
- URLSearchParams
- URLPattern
- URL
- TrustedTypePolicyFactory
- TrustedTypePolicy
- TrustedScriptURL
- TrustedScript
- TrustedHTML
- TransformStreamDefaultController
- TransformStream
- TextMetrics
- TextEncoderStream
- TextEncoder
- TextDecoderStream
- TextDecoder
- TaskSignal
- TaskPriorityChangeEvent
- TaskController
- SyncManager
- SecurityPolicyViolationEvent
- Scheduler
- Response
- Request
- ReportingObserver
- ReadableStreamDefaultReader
- ReadableStreamDefaultController
- ReadableStreamBYOBRequest
- ReadableStreamBYOBReader
- ReadableStream
- ReadableByteStreamController
- RTCEncodedVideoFrame
- RTCEncodedAudioFrame
- PromiseRejectionEvent
- ProgressEvent
- PerformanceServerTiming
- PerformanceResourceTiming
- PerformanceObserverEntryList
- PerformanceObserver
- PerformanceMeasure
- PerformanceMark
- PerformanceEntry
- Performance
- Path2D
- OffscreenCanvasRenderingContext2D
- OffscreenCanvas
- NetworkInformation
- MessagePort
- MessageEvent
- MessageChannel
- MediaCapabilities
- ImageData
- ImageBitmapRenderingContext
- ImageBitmap
- IDBVersionChangeEvent
- IDBTransaction
- IDBRequest
- IDBOpenDBRequest
- IDBObjectStore
- IDBKeyRange
- IDBIndex
- IDBFactory
- IDBDatabase
- IDBCursorWithValue
- IDBCursor
- Headers
- FormData
- FontFace
- FileReaderSync
- FileReader
- FileList
- File
- EventTarget
- EventSource
- Event
- ErrorEvent
- DedicatedWorkerGlobalScope
- DecompressionStream
- DOMStringList
- DOMRectReadOnly
- DOMRect
- DOMQuad
- DOMPointReadOnly
- DOMPoint
- DOMMatrixReadOnly
- DOMMatrix
- DOMException
- CustomEvent
- Crypto
- CountQueuingStrategy
- CompressionStream
- CloseEvent
- CanvasPattern
- CanvasGradient
- CanvasFilter
- CSSSkewY
- CSSSkewX
- ByteLengthQueuingStrategy
- BroadcastChannel
- Blob
- AbortSignal
- AbortController
- name
- onmessage
- onmessageerror
- cancelAnimationFrame
- close
- postMessage
- requestAnimationFrame
- webkitRequestFileSystem
- webkitRequestFileSystemSync
- webkitResolveLocalFileSystemSyncURL
- webkitResolveLocalFileSystemURL
- Atomics
- WebAssembly
- AudioData
- EncodedAudioChunk
- EncodedVideoChunk
- ImageTrack
- ImageTrackList
- VideoColorSpace
- VideoFrame
- AudioDecoder
- AudioEncoder
- ImageDecoder
- VideoDecoder
- VideoEncoder
- BarcodeDetector
- Cache
- CacheStorage
- CryptoKey
- Lock
- LockManager
- NavigationPreloadManager
- ServiceWorkerRegistration
- StorageManager
- SubtleCrypto
- WebTransport
- WebTransportBidirectionalStream
- WebTransportDatagramDuplexStream
- WebTransportError
- FileSystemDirectoryHandle
- FileSystemFileHandle
- FileSystemHandle
- FileSystemWritableFileStream
- FileSystemSyncAccessHandle
- IdleDetector
- MediaSource
- SourceBuffer
- SourceBufferList
- Serial
- SerialPort
- USB
- USBAlternateInterface
- USBConfiguration
- USBConnectionEvent
- USBDevice
- USBEndpoint
- USBInTransferResult
- USBInterface
- USBIsochronousInTransferPacket
- USBIsochronousInTransferResult
- USBIsochronousOutTransferPacket
- USBIsochronousOutTransferResult
- USBOutTransferResult
Worker を使う
実際にどうやって使用するのか、どう記述すると使えるのかを見ていきたいと思います。
その後にこの仕組みがどう動くのかも解説していきます。
ブラウザ側
<script>
const browserWorker = new Worker('/path/to/worker.js');
const data = '送り込みたいデータ: 文字列 or [] or {}';
browserWorker.postMessage(data);
browserWorker.onmessage = (event) => {
console.log(event.data);
browserWorker.terminate();
}
</script>
Worker側
self.onmessage = (event) => {
console.log(event.data);
const returnData = '処理済みのデータ';
postMessage(returnData);
close();
}
上記は最低限の記述となっていて、
HTML側はデータを送信して受け取る
Worker側はデータを受け取って送信する
だけの処理になっています。
処理の流れ
-
ブラウザ側:1行目
Workerに処理したい内容が記述されているJSファイルのパスを、Workerメソッドに引数として読み込ませることで、
インスタンスが作成され準備が整います。 -
ブラウザ側:2行目
Workerに処理してほしいデータを作ります。
このとき注意する点は、データに関数が含まれている場合は、Workerへのデータ送信時に Error(DOMException) になってしまいます。 -
ブラウザ側:3行目
Worker側にデータを送ります。 -
Worker側:1行目
Worker内では this ではなく、 self を使用しています。(onmessage内では this == self になります)
ブラウザ側からデータを受け取るイベント onmessage に処理内容である関数を代入します。
このとき引数として落ちてくるものは MessageEvent です。 -
Worker側:2行目
ブラウザ側からデータが来ているかどうかの確認は、引数の evt の evt.data に確認できます。 -
Worker側:3行目
returnData何かしらの処理を行ったデータを変数に格納しています。 -
Worker側:4行目
処理済みのデータをブラウザ側に返しています。 -
Worker側:5行目
Workerを閉じます。
Workerを閉じた場合、ブラウザ側から再度データを送信しても動作しません。
再度、Workerをインスタンスから作る必要があります。 -
ブラウザ側:5行目
Worker側からのデータを受け取るイベント onmessage に関数を代入します。
これは addEventListener('message', function(){}) でも代替は可能です。 -
ブラウザ側:6行目
Worker側からデータが来ているかどうかの確認は、引数の evt の evt.data で確認できます。 -
ブラウザ側:7行目
ブラウザ側でWorkerを閉じる場合は terminate を実行します。
Workerを使用する上での注意点
流れの中でも注意点を記載しましたが、それ以外にも使用する上でいくつかの注意点があるので、記載したいと思います。
- worker.jsはデータが送られてくるまで動作しません。立ち上げただけでは自動で動かないので、何でもいいのでデータを送って起こしてあげてください。
- 上記でも記載した通り、Worker側に関数を含めたデータを送ることはできません。
- Worker側からブラウザ側にも関数を含めたデータを送ることはできません。
- 処理2〜9までの間は非同期の処理になるので、ブラウザ側にデータがいつ返ってくるのかが分かりません。
- ブラウザ側で受け取ったデータを使用してHTMLに何か行う場合、早すぎるor遅すぎる可能性があります。実行タイミングにはきおつけてください。
- Workerを閉じた場合は再度インスタンスを立てる必要があります。
まとめ
完了までに時間のかかる処理をWorkerに一部渡すことで、
ウェブサイトの描画をブロックする時間を短くすることができるようになりました。
これを利用して、
Workerに複数の外部APIからデータを取得させて、取得した各APIのデータを組み合わせて必要な形に成形したのち、
ブラウザに送り返すという処理を実装したことがあります。
擬似的にAPIサーバーとして動かすという形を取りましたが、問題無く動作しました。
取得までに時間ががかる場合は、スケルトンスクリーンを表示することで待機時間の長さを感じさせない工夫も組み合わせるなどの実装も施しました。
Workerの使い所としてはまだまだあると思うので新たな使い方が見つかればそのことについて書きたいと思います。