TECH PLAY

Vue.js

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

はじめに NTT西日本の中川です。 本記事では、ブラウザのメインスレッドを占有しないよう Web Worker を使って重い処理をバックグラウンドへ逃がす方法を、実験コードつきで解説します。シングルスレッドのJavaScriptでも、UXを落とさずに計算処理と描画を両立するための実装パターンをまとめました。 本記事は2026年3月時点の情報に基づきます。 仕様書やMDNなどを読んでも、「Workerに逃がす」と口で言うのと、 画面上で動きの差を一度見る のとでは、腹落ちの深さがかなり違います。私自身も最初の頃はイベントループの説明と体感がなかなか結びつかず、実験用の短い for を回して初めて「占有」の意味が自分のものになった経験があります。同じように手を動かしながら確認したい方に向け、デモを多めに載せていますので、ぜひ試してみてください。 設計思想やデバッグ手法など、コードの品質を高める方法はさまざまです。しかし、どれほどきれいなコードを書いても、ユーザー体験(UX)を損なう「画面のフリーズ」が発生してしまうと、アプリケーションの価値は大きく損なわれてしまいます。 JavaScriptはメインスレッド上でUIとスクリプトが順番に処理されるため、重い処理をそのまま走らせると描画が止まります。今回は、その限界を緩和する標準機能である Web Worker に焦点を当ててご紹介します。 対象読者 本記事が想定する対象読者は以下の通りです。 JavaScriptのイベントループや実行コンテキストなど基礎知識をお持ちの方 大量データの加工や複雑な計算で、画面が一瞬カクついたり止まったりする課題を抱えている方 「JavaScriptはシングルスレッドだから重い処理は避けるべき」という前提の先にある、並行に近い実行モデルを知りたい方 目次 はじめに 対象読者 目次 1.背景・目的 2.スレッドとは 3.なぜ「画面が止まる」のか(イベントループの制約) 実験:メインスレッドをブロックしてみる 結果 4.Web Workerとは何か 実装時の主なルール 5.【実践】Web Workerを導入してフリーズを和らげる ステップ1:worker.js ステップ2:index.html(メインスレッド側) 結果 補足:メインとWorkerのやり取りのコスト 6.よくあるユースケースと簡易コード(何が起きるかをセットで理解する) ケース1:回転する「動き続けるUI」と重い処理(ループ) 結果 ケース2:進捗の逐次通知(重い処理のプログレスバー) 結果 7.使い分け:Workerを使うべきとき・避けた方がよいとき 検証ツールで処理負荷を“測定”して可視化する Workerが向いている処理の例 Workerが向かない、または慎重になる処理の例 8.さらに高度な活用:転送可能オブジェクト(Transferable) 9.まとめ 執筆者 参考資料・出典 商標 1.背景・目的 モダンなWebアプリケーションでは、ブラウザ上で実行されるロジックが肥大化し続けています。一方で、ユーザーは滑らかなアニメーションと即座のレスポンスを期待しているケースが多いです。 本記事の目的は、 メインスレッドをUI応答に使い続けつつ、重い計算をWorker側へ委ねる という構成を、実際にブラウザで動かしながら体感し、実装の型を身につけていただくことです。 業務のコードでも、いったんメインに載せた処理を抱え込んだまま進め、 計測で長いタスクがはっきり見えてから Workerへ切り出す、という順番になりがちです。後から直せばよいのですが、手戻りが発生するとどんどん期間的な余裕がなくなっていくケースが多いと思います。だからこそ設計の早い段階で「メインはUIと短い応答に寄せる」という前提を共有しておくと楽になる場面が、私がこれまで経験してきた案件でも少なくありませんでした。本記事は、その前提づくりのための土台として整理しました。 2.スレッドとは OSやCPUの厳密な定義に踏み込むと長くなるため、 この記事を読むための最小限のイメージ だけを解説します。 簡易な説明となりますので、あくまでイメージと捉えてください。 スレッド(thread) 処理の流れを運ぶ 作業ライン のようなものです。レストランでいえば、調理や提供を進めるための「通路」に近いイメージです。 シングルスレッド 同時に使える通路が1本だけ で、そこに仕事が順番に並ぶ状態です。JavaScriptの メインスレッド は、基本この「1本の通路」上で動きます。 メインスレッド ブラウザでは、この1本の通路のうち、 画面の表示やユーザー操作(クリック・入力など)への反応を扱う主役のライン を指すことが多いです。本文では「メイン」と略します。 マルチスレッド(複数スレッド) 複数の通路を並行して使える イメージです。Web Workerは、メインとは別のスレッド上でスクリプトを実行できます。ただし WorkerからはDOM(Document Object Model)を直接操作できない など、制約があります。 この記事では「 重い計算をメインの通路にずっと置くと、画面の更新が後回しになる。だからWorkerという別の通路に逃がす 」というイメージを持って読み進めていただくと、後の内容を理解する助けになります。 ※用語の厳密な定義や、ブラウザ実装ごとの差異は、参考資料のMDNや仕様書で確認してください。 3.なぜ「画面が止まる」のか(イベントループの制約) JavaScriptの実行環境(メインスレッド)は、 UIの描画更新とスクリプトの実行を、ひとつのキューに乗ったタスクとして順番に処理する モデルが基本です(イベントループ)。 メインスレッドとイベントループの参考イメージ(タスクキューから実行・重い処理時のブロック) ※図内の「rAF」とは requestAnimationFrame() の略です。 上段は「順番待ちのモデル」、下段は「 重い1件の間、ほかの仕事が先に進みにくい 」ことをイメージした図です(厳密なブラウザ内部モデルではなく、説明用の略図です)。 例えば、数秒かかるループ処理をメインスレッドで実行すると、その間は「次の描画」に回る前に長いタスク占有が発生します。結果として、アニメーションやクリック応答が止まったように見えます。これがいわゆるメインスレッドのブロックです。 実験:メインスレッドをブロックしてみる 以下を index.html として保存し、ローカルで開いて試してください( file:// ではなく 簡易HTTPサーバー で配信する方が挙動が安定します)。 <!doctype html> < html lang = "ja" > < head > < meta charset = "utf-8" /> < meta name = "viewport" content = "width=device-width,initial-scale=1" /> < title > メインスレッドをブロックする実験 </ title > </ head > < body > < h2 > メインスレッドをブロックする実験 </ h2 > < p id = "status" > 待機中です。ボタンで重い処理を開始します。 </ p > < pre id = "log" style = "background:#f5f5f5;padding:8px;border-radius:4px;font-size:13px;" ></ pre > < div id = "box" style = "width:50px; height:50px; background:red; position:relative;" ></ div > < div > < small > このボタンでは、わざと「重い処理」を走らせて、画面(メインスレッド)が止まる感じを体感します。 < br /> 走る計算は2つです。 < br /> < br /> 1) < b > 累積和(計算結果の表示) </ b >< br /> 0〜n-1 の合計(累積和)を表示します。 < br /> < br /> 2) < b > 負荷をかけるループ(フリーズ体感用) </ b >< br /> こちらはn回ループしてCPUを使い、メインスレッドを占有させるための処理です。 < br /> < code > dummy </ code > はループ中に更新する作業用の変数で、値そのものに意味はありません。 < br /> なお内部的には32bit整数として更新しているため、表示は符号なし(0〜4,294,967,295)に変換しています。 < br /> < br /> </ small > </ div > < button id = "runBtn" type = "button" > 重い処理を実行(フリーズします) </ button > < script > const statusEl = document . getElementById ( "status" ) ; const logEl = document . getElementById ( "log" ) ; const runBtn = document . getElementById ( "runBtn" ) ; let pos = 0 ; function animate () { pos = ( pos + 2 ) % 300 ; document . getElementById ( "box" ) . style . left = pos + "px" ; requestAnimationFrame ( animate ) ; } animate () ; function fmtTime ( d ) { return ( d . toLocaleTimeString ( "ja-JP" , { hour12 : false }) + "." + String ( d . getMilliseconds ()) . padStart ( 3 , "0" ) ) ; } function heavyTask () { logEl . textContent = "" ; runBtn . disabled = true ; statusEl . textContent = "次の描画フレームのあと、重い処理を開始します…" ; requestAnimationFrame (() => { requestAnimationFrame (() => { const wallStart = new Date () ; statusEl . textContent = "実行中です(メインスレッド占有中)。赤いボックスの動きと、この文が更新されない時間に注目してください。" ; const t0 = performance . now () ; const n = 1_000_000_000 ; let dummy = 0 ; for ( let i = 0 ; i < n ; i ++ ) { dummy = ( dummy + ( i & 7 )) | 0 ; } const dummyU32 = dummy >>> 0 ; const result = ( BigInt ( n - 1 ) * BigInt ( n )) / 2 n ; const elapsed = Math . round ( performance . now () - t0 ) ; const wallEnd = new Date () ; statusEl . textContent = "重い処理が終わりました。メインスレッドが解放され、ボックスが再び動き出します。" ; logEl . textContent = "【画面ログ】\n" + "開始時刻: " + fmtTime ( wallStart ) + "\n" + "終了時刻: " + fmtTime ( wallEnd ) + "\n" + "所要時間: " + elapsed + " ms\n" + "累積和 : " + result . toString () + "\n" + "ダミー値(符号なし32bit): " + dummyU32 ; runBtn . disabled = false ; }) ; }) ; } runBtn . addEventListener ( "click" , heavyTask ) ; </ script > </ body > </ html > 結果 メインスレッドをブロックしてみるの参考イメージ ボタンを押すと、赤いボックスの動きが止まります。これがメインスレッドが長時間占有されている状態です。あわせて、 実行中は画面上部の状態メッセージが更新されない (フリーズしている)こと、 終了後にログ欄へ開始・終了時刻と所要時間がまとめて表示される ことも確認できます。ループ回数や端末性能など、かかる時間は環境ごとに異なります。 所要時間に表示された時間分だけ、メインスレッドが占有されていたということになります。 4.Web Workerとは何か Web Workerは、Webアプリケーションの メイン実行スレッドとは別のバックグラウンド上でスクリプトを動かす仕組み です(実装はブラウザ依存ですが、「メインとは別の並行の実行コンテキスト」という理解で問題ありません)。重い計算をWorkerに逃がすと、メイン側はブロックされにくくなり、ユーザーの操作や画面の再描画を順に進めやすくなります。しかし、メインとWorkerの間のメッセージ処理そのものもメインスレッドで扱うため、通信のやりすぎは別のボトルネックになる可能性があり、注意が必要です。 実装時の主なルール スクリプトはURLとして渡して起動する :別ファイルの .js を指すのが一般的ですが、Blobから生成したURLのように、ファイルを分けずに渡す方法もあります。読み込むWorkerスクリプトは 呼び出し元と同一オリジン であることが基本で、クロスオリジンで読み込む場合は追加の条件(CORSなど)が必要です。 DOM操作は不可 :Workerの実行環境には window や document はなく、ページのDOMを直接触れません(仕様上、Dedicated Workerのグローバルは WorkerGlobalScope 系です)。 メッセージでやり取りする : postMessage と message イベント(または addEventListener('message',…) )でデータを受け渡しします。 不要になったら終了する :都度Workerを新規作成する場合は、処理完了後に terminate() するか、 1つのWorkerを使い回す 設計にします(放置するとリソースを食い続けます)。 5.【実践】Web Workerを導入してフリーズを和らげる 先ほどの実験を、Web Workerで書き直します。同じフォルダに index.html と worker.js を置いてください。 ここで表したいのは、「非常に重いCPU計算(= メインスレッドを占有しがちなループ)」をWorkerへ逃がすことで、メイン側のアニメーション(赤いボックス)が止まらない、という効果です。 この例で画面に出している数値(累積和)は、 「処理が最後まで完了した」ことを確認するための目印 です。 経過時間ではないことに注意してください。 Worker用のスクリプトは、 HTMLに <script src="worker.js"> と書いて読み込みません 。メイン側の new Worker('worker.js') のときに、ブラウザが別スレッド用として worker.js を取得・実行します(メイン用の <script> で読むと、Worker向けコードがメインスレッドで動いてしまい意図とずれます)。 ステップ1: worker.js // 重いループを最後まで回し切った「完了の証拠」として値をメインへ返す。 self .onmessage = function () { const n = 1_000_000_000 ; // ダミー計算(CPUを使う/32bitで回し続けて“走った痕跡”を残す) let dummy = 0 ; for ( let i = 0 ; i < n ; i ++ ) { dummy = ( dummy + ( i & 7 )) | 0 ; } const dummyU32 = dummy >>> 0 ; // 結果の目印(0〜n-1 の合計を BigInt で厳密に) const sum = ( BigInt ( n - 1 ) * BigInt ( n )) / 2 n ; self . postMessage ({ n , sum : sum . toString () , dummy : dummyU32 }) ; } ; メインに届く値: 上記の postMessage(result) の引数が、メイン側の message ハンドラでは e.data として受け取れます。 ステップ2: index.html (メインスレッド側) 先ほどの実験コードのうち、 ボタンを押したときの処理(重いループ部分)だけ をWorker呼び出しに差し替えた例です。ボタン連打でWorkerが増殖しないよう、 前回のWorkerは終了してから 新しく起動しています。ログ欄に出す e.data は、 ステップ1の result と同じ値 です。 <!doctype html> < html lang = "ja" > < head > < meta charset = "utf-8" /> < meta name = "viewport" content = "width=device-width,initial-scale=1" /> < title > Web Workerで計算を逃がす </ title > </ head > < body > < h2 > Web Workerで計算を逃がす </ h2 > < p id = "status" > ボタンでWorkerに計算を依頼できます。 </ p > < pre id = "log" style = "background:#f5f5f5;padding:8px;border-radius:4px;font-size:13px;min-height:3em;" > (結果はここに表示されます) </ pre > < div > < small > このボタンでは、重い計算をWorkerに任せて、画面(メインスレッド)の描画が止まりにくくなることを体感します。 < br /> Workerが返す値は2つです。 < br /> < br /> 1) < b > 累積和(計算結果の表示) </ b >< br /> 0〜n-1 の合計(累積和)を表示します。 < br /> < br /> 2) < b > ダミー値(符号なし32bit) </ b >< br /> Worker側で負荷をかけるループの中で更新している作業用の値です(値そのものに意味はありません)。 < br /> </ small > </ div > < div id = "box" style = "width:50px; height:50px; background:red; position:relative;" ></ div > < br /> < button type = "button" id = "runBtn" > 重い処理をWorkerへ(描画は動き続ける想定) </ button > < script > const statusEl = document . getElementById ( "status" ) ; const logEl = document . getElementById ( "log" ) ; const runBtn = document . getElementById ( "runBtn" ) ; let pos = 0 ; function animate () { pos = ( pos + 2 ) % 300 ; document . getElementById ( "box" ) . style . left = pos + "px" ; requestAnimationFrame ( animate ) ; } animate () ; let myWorker = null ; function heavyTask () { logEl . textContent = "" ; runBtn . disabled = true ; statusEl . textContent = "Workerへ処理を送りました。計算中は画面の指示に従い、赤いボックスの動きも見てください。" ; if ( myWorker ) { myWorker . terminate () ; } try { myWorker = new Worker ( "worker.js" ) ; } catch ( err ) { statusEl . textContent = "Workerを起動できませんでした。" ; logEl . textContent = "起動エラー: " + ( err && err . message ? err . message : String ( err )) + "\n" + "(worker.js のパス、http:// で開いているか、ブラウザ設定などを確認してください)" ; myWorker = null ; runBtn . disabled = false ; return; } myWorker . onmessage = function ( e ) { // worker.js は { sum, dummy } の形で返す想定ですが、コピペのズレ等で形が違う場合もあり得るので // 表示は防御的に扱います。 const data = e . data ; const sum = data && typeof data === "object" ? ( data . sum ?? data . result ) : data ; const dummy = data && typeof data === "object" ? data . dummy : undefined ; const endTime = new Date () . toLocaleTimeString ( "ja-JP" , { hour12 : false , }) ; statusEl . textContent = "Workerから結果を受け取りました(" + endTime + ")。" ; logEl . textContent = "累積和(0〜999,999,999 の合計): " + sum + "\n" + "ダミー値(符号なし32bit): " + ( dummy ?? "(未取得)" ) ; myWorker . terminate () ; myWorker = null ; runBtn . disabled = false ; } ; myWorker . onerror = function ( e ) { statusEl . textContent = "Workerの読み込み、または実行でエラーが発生しました。" ; logEl . textContent = "エラー: " + ( e && e . message ? e . message : "(詳細不明)" ) + "\n" + "(worker.js が存在しない、パスが違う、http:// で開いていない、同一オリジンでないなどの可能性があります)" ; myWorker . terminate () ; myWorker = null ; runBtn . disabled = false ; } ; myWorker . onmessageerror = function () { statusEl . textContent = "Workerとのメッセージの受け渡しに失敗しました。" ; logEl . textContent = "messageerror: Workerから受け取ったデータを復元できませんでした。" ; myWorker . terminate () ; myWorker = null ; runBtn . disabled = false ; } ; myWorker . postMessage ( null ) ; } runBtn . addEventListener ( "click" , heavyTask ) ; </ script > </ body > </ html > 結果 Web Workerを導入してフリーズを和らげるの参考イメージ 先ほどとは異なり、ボタン押下中も赤いボックスのアニメーションが動き続け、数秒後に ログ欄へ累積和(0〜999,999,999の合計) が表示されます。状態メッセージで、送信中・受信済みの流れも追えます。挙動はブラウザ実装・CPU負荷・他タブの状況により変わるため、 可能であれば手元で確認 してください。 補足:メインとWorkerのやり取りのコスト メインからWorkerへデータを渡す処理では、原則として データの複製 が発生します。巨大なオブジェクトを頻繁に往復させると、通信そのものがボトルネックになることがあるため、注意してください。コピーを避ける高度な手段は本記事の後半で紹介します。 6.よくあるユースケースと簡易コード(何が起きるかをセットで理解する) ここからは 1ファイルで試せる 例です。 new Worker(URL.createObjectURL(blob)) により、別ファイルを置かずにWorkerスクリプトを渡しています(本番ではファイル分割やバンドラ連携が一般的です)。 各コードの直後に、 ブラウザ上でどう見えるか・何が得られるか をまとめています。 ファイルを増やしたくないときに Blob からWorkerを起動するのは、個人的にも手習い向きで重宝しています。本番とは設計が異なる点は後述の通りですが、「まず挙動を掴む」には向いています。 ケース1:回転する「動き続けるUI」と重い処理(ループ) 重いCPUループは本来メインを占有しやすいことはこれまでの内容でお伝えしてきましたが、ここでは 回転する赤い四角 を置き、 「止まらない=メインの描画処理が生きている」 ことを一目で確認できるように画面上の「経過時間(メイン)」は、メインが動いている間だけ増え続けるようにしています。 Worker処理完了時に出る 巨大な数値(累計) は、ループを最後まで回し切った 完了の目印 です。 <!doctype html> < html lang = "ja" > < head > < meta charset = "utf-8" /> < meta name = "viewport" content = "width=device-width,initial-scale=1" /> < title > ケース1:回転UIと重い処理(Worker) </ title > </ head > < body > < h3 > 回転が止まらないか、注目してみてください </ h3 > < p > 赤い四角は < code > requestAnimationFrame </ code > で回転しています。 </ p > < div > < small > この例は、重い計算をWorkerに任せても「メイン側の描画(回転)」が止まりにくいことを体感するためのものです。 < br /> < br /> - 確認するポイント: < b > 赤い四角の回転 </ b > と < b > 経過時間 </ b > が止まらないか < br /> - Workerが返す値: < b > 累積和(計算結果) </ b > と < b > ダミー値(符号なし32bit) </ b >< br /> ※ダミー値は負荷をかけるループ中に更新している作業用の値で、値そのものに意味はありません。 < br /> </ small > </ div > < div id = "spin" style = "width:48px;height:48px;background:#d33;margin:12px 0;transform-origin:center center;" ></ div > < p > 経過時間(メイン): < span id = "tick" > 0 </ span > ms(フレームが進むほど増えます) </ p > < p id = "st" > 待機中 </ p > < button type = "button" id = "run" > Workerで重いループ(約2億ステップ) </ button > < script > const spinEl = document . getElementById ( "spin" ) ; const tickEl = document . getElementById ( "tick" ) ; const st = document . getElementById ( "st" ) ; const runBtn = document . getElementById ( "run" ) ; let deg = 0 ; ( function spinLoop () { deg = ( deg + 4 ) % 360 ; spinEl . style . transform = "rotate(" + deg + "deg)" ; requestAnimationFrame ( spinLoop ) ; })() ; let t0 = performance . now () ; ( function tickLoop () { tickEl . textContent = String ( Math . round ( performance . now () - t0 )) ; requestAnimationFrame ( tickLoop ) ; })() ; runBtn . onclick = () => { runBtn . disabled = true ; st . textContent = "Workerで計算中…(この間も回転と経過表示が止まらないはず)" ; const src = [ "self.onmessage = () => {" , " const n = 200000000;" , " let dummy = 0;" , " for (let i = 0; i < n; i++) dummy = (dummy + (i & 7)) | 0;" , " const dummyU32 = dummy >>> 0;" , " const sum = (BigInt(n - 1) * BigInt(n)) / 2n;" , " self.postMessage({ steps: n, sum: sum.toString(), dummy: dummyU32 });" , "};" , ] . join ( "\n" ) ; const w = new Worker ( URL . createObjectURL ( new Blob ([ src ] , { type : "application/javascript" }) , ) , ) ; w . onmessage = ( e ) => { st . textContent = "完了(Worker): ステップ数 " + e . data . steps + " の累積和 = " + e . data . sum + "(ダミー値・符号なし32bit: " + e . data . dummy + ")" ; runBtn . disabled = false ; w . terminate () ; } ; w . onerror = ( e ) => { st . textContent = "Workerエラー: " + e . message ; runBtn . disabled = false ; w . terminate () ; } ; w . postMessage ( null ) ; } ; </ script > </ body > </ html > 結果 ケース1:回転する「動き続けるUI」と重い処理(ループ)の参考イメージ このケースでは、ボタン押下後も 赤い四角の回転が途切れない ので、「重い処理」をWorkerへ逃がしたときの体感が掴みやすいです。 「経過時間(メイン)」 が止まらず増え続けるのは、メインがイベントループと描画を進め続けている証拠となります。 最後に表示される 巨大な累計 は、約2億ステップ分のループを終えた結果の 完了の目印 です。端末性能により待ち時間は変わります。長すぎる場合はWorker内の n を小さく調整してください。 ケース2:進捗の逐次通知(重い処理のプログレスバー) Worker内でループを区切り、 何%進んだか を何度かメインへ送り、画面上のプログレスバーだけを更新してみます。 <!doctype html> < html lang = "ja" > < head > < meta charset = "utf-8" /> < meta name = "viewport" content = "width=device-width,initial-scale=1" /> < title > ケース2:進捗の逐次通知(Worker) </ title > </ head > < body > < div > < small > この例は、Workerから「いま何%まで進んだか」を何回か受け取り、プログレスバーだけを更新するサンプルです。 < br /> 見どころは、重い計算中でも < b > バーが段階的に伸びる </ b > 点です。 < br /> ※最後に返ってくる < code > work </ code > は、Worker側の重いループで作った「最終的な計算値」です。 < br /> 処理時間ではなく、あくまで「最後まで計算が走り切った」ことを確認するために表示しています(中身を解釈する必要はありません)。 < br /> </ small > </ div > < div id = "bar" style = "height:12px;width:0;background:#06c;" ></ div > < p id = "pct" > 0% </ p > < button type = "button" id = "run" > Workerで進捗付き処理 </ button > < script > const runBtn = document . getElementById ( "run" ) ; runBtn . onclick = () => { if ( runBtn . disabled ) return; runBtn . disabled = true ; document . getElementById ( "bar" ) . style . width = "0%" ; document . getElementById ( "pct" ) . textContent = "0%" ; const src = ` self.onmessage = () => { const steps = 5; let work = 0; for (let s = 1; s <= steps; s++) { let x = 0; for (let i = 0; i < 80000000; i++) x += i % 7; work = x; self.postMessage({ progress: Math.round((s / steps) * 100) }); } self.postMessage({ done: true, work }); }; ` ; const w = new Worker ( URL . createObjectURL ( new Blob ([ src ] , { type : "application/javascript" }) , ) , ) ; w . onmessage = ( e ) => { if ( "progress" in e . data ) { const p = e . data . progress ; document . getElementById ( "bar" ) . style . width = p + "%" ; document . getElementById ( "pct" ) . textContent = p + "%" ; } if ( e . data . done ) { document . getElementById ( "pct" ) . textContent += " (完了 / work=" + e . data . work + ")" ; runBtn . disabled = false ; w . terminate () ; } } ; w . onerror = function () { document . getElementById ( "pct" ) . textContent += " (Workerエラー)" ; runBtn . disabled = false ; w . terminate () ; } ; w . postMessage ( null ) ; } ; </ script > </ body > </ html > 結果 ケース2:進捗の逐次通知(重い処理のプログレスバー)の参考イメージ 青いバーの幅が 20% → 40% → … → 100% のように段階的に伸びる(Workerから進捗メッセージが届くたびに更新)。 完了後に「(完了)」が付く。内側のループ回数は端末に合わせて調整してください(軽すぎると一瞬で終わり、重すぎると待ち時間が長くなります)。 実行中はボタンが無効化され、連打でWorkerが複数起動しにくいようにしています。 補足: 進捗通知を細かくしすぎると、メインとWorkerの間のやり取りの回数が増え、かえってオーバーヘッドになることがあります。実務では一定間隔・一定チャンクごとに送るのが無難です。 7.使い分け:Workerを使うべきとき・避けた方がよいとき 検証ツールで処理負荷を“測定”して可視化する ブラウザの検証ツールを使うと処理負荷を可視化することができ、一層イメージが持ちやすいです。 DevToolsを開く → Performance DevToolsを開く → Performance 記録開始(Record)→ サンプルのボタンを押して重い処理を走らせる → 停止 記録開始(Record)→ サンプルのボタンを押して重い処理を走らせる → 停止 タイムライン上で、メインスレッドの Long Task(長いタスク) や、描画(Frames)の詰まり方を確認する タイムライン上で、Worker処理が確認できる 「メインで実行した場合」と「Workerに逃がした場合」で、メインスレッド上の長い塊の出方が変わるのがポイントです。 ここまででWorkerの便利な動きについて紹介してきましたが、 「何でもWorkerに投げればよい」わけではありません。 どんな機能にも向き不向きがあり、Workerも例外ではありません。 それぞれ一部を紹介します。 Workerが向いている処理の例 数十万件以上の配列のフィルタリング・集計・変換 画像やバイナリの加工、暗号処理など、DOMに触れない重い計算 インタラクティブなUIの応答性を優先し、メインスレッドの占有時間を短く保ちたい場面 Workerが向かない、または慎重になる処理の例 数ミリ秒で終わる軽い計算(起動とメッセージのオーバーヘッドの方が大きくなることがある) DOMを頻繁に更新する処理(Workerからは直接操作できないため、結果をメインへ戻して反映が必要) 巨大なデータを高頻度で往復させる処理(コピーまたは転送戦略の設計が必須) 8.さらに高度な活用:転送可能オブジェクト(Transferable) 巨大なバイナリデータ(型付き配列の裏側のバッファなど)をWorkerへ送る場合、デフォルトの複製ではメモリと時間を消費します。メッセージを送るAPIのオプションで転送リストを渡すと、バッファの所有権をWorker側へ移し、コピーを避けられる場合があります(転送後、メインスレッド側ではそのバッファは利用できなくなります)。 高度な機能となるため、ここではほんの少しだけ、一部を紹介します。 // メイン側(例:100MBのバッファを転送) const buffer = new Uint8Array ( 1024 * 1024 * 100 ) . buffer ; myWorker . postMessage ({ type : 'binary' , payload : buffer } , [ buffer ]) ; // 転送後、メインスレッド側では buffer はデタッチされ、触れない想定になる Worker側では、メインが送ったデータの中身として受け取ります。用途に応じて複数スレッドで同じメモリを共有する仕組みを検討する道もありますが、 サイト全体のセキュリティ設定(クロスオリジン隔離など)が必要になってくる ため、まずは同一オリジンでさまざまな使い方を模索してみるのが良いと思います。 9.まとめ Web Workerを使うと、JavaScriptのシングルスレッドという制約のもとでも、メインスレッドの占有時間を減らし、UXと重い計算の両立を狙いやすくなります。 ボトルネックの特定 :ブラウザの開発者ツールで、長いタスクがどこで発生しているかを把握する(性能分析の画面や、計測用のAPIを使う方法があります)。 純粋な計算の分離 :画面の要素を直接いじらない処理をWorkerへ切り出す。 メッセージ設計 :やり取りするデータのサイズと頻度を抑え、必要なら後述の「転送」による最適化で転送コストを下げる。 「ブラウザ向けJavaScriptは重い処理に向かない」と言われがちですが、Web Workerのような標準機能を適切に組み合わせれば、フロントエンドでも実用的なアプリケーションを構築できます。 ループ回数は端末の性能などで体感が変わります。動かしてみて「差が分かりにくい」と感じたら、本記事の3節目のブロック実験だけループを半分にしてみたり、5節目のサンプルにてWorker側はそのまま、といった 片側だけ編集してみるなどの比較 を試してみてください。私も記事を整えるときは、何度かこの比較に立ち返って文言を直しました。開発者ツールの性能パネルを開いたまま読み返すと、長いタスクの見え方もイメージしやすくなるはずです。是非試してみてください。 執筆者 中川 拓哉(NTT西日本 デジタル革新本部 デジタル改革推進部所属) NTT西日本のWebアプリケーションの開発・運営に従事。 好きな技術スタック:TypeScript, Vue.js, GraphQL, Laravel 参考資料・出典 本記事を執筆するにあたり、以下のサイト・資料を参考にしました。 MDN Web Docs — Web Workers API MDN Web Docs — Worker MDN Web Docs — postMessage HTML Living Standard — postMessage with transfer WHATWG HTML — Safe passing of structured data 商標 「JavaScript」は、Oracle Corporation およびその子会社の米国およびその他の国における商標または登録商標です。 「Google Chrome」は、Google LLC の商標です。 記載のその他の会社名・製品名は、それぞれ各社の商標もしくは登録商標です。
こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 最近、 VoidZero から Vite+ がリリースされました。 Vite+ は Vite 8、Vitest、Oxlint、Oxfmt などを統合した「Web のための統合ツールチェーン」です。 駅メモ!は現在 Vue + Vite 7 + Vitest + ESLint(チーム独自ルール有り)+ Prettier + Stylelint で開発されており、Vite+ のツールはまだ導入していません。 AI で開発が高速化した現代、数十倍速いと謳う Vite+ のツールチェーンは気になります。 ただ、そもそも各ツールがどれくらい Vue 対応しているのか、どう設定すれば良いのかがわからなかったので、テンプレを使って確認することにしました。 Vite+ 経由の vp create vue と従来の pnpm create vue@latest で生成されるプロジェクト設定を比較し、Vue プロジェクト特有の注意点を整理します。 検証環境 プロジェクトの作成 Vite+ 経由 従来の create-vue 生成される設定ファイルの比較 Vite+ プロジェクトの構成 従来の create-vue プロジェクトの構成 共通: eslint.config.ts 共通: Lint 実行順 Linter 比較: Oxlint vs ESLint 検証用コンポーネント Oxlint の結果 ESLint の結果 CSS/Style Lint について 型チェックの違い テスト Formatter 比較: Oxfmt vs Prettier Vue SFC のフォーマット対応 全体比較表 まとめ Linter 型チェック Formatter CSS Lint 検証環境 vp v0.1.16 (Vite+) create-vue v3.22.2 (pnpm create vue@latest) Node.js v24.14.1 pnpm v10.33.0 プロジェクトの作成 Vite+ 経由 vp create vue vue-viteplus ◇ Which package manager would you like to use? pnpm ◇ pnpm@10.33.0 installed ◇ Which agents are you using? Claude Code ◇ Which editor are you using? VSCode ◇ Set up pre-commit hooks to run formatting, linting, and type checking with auto-fixes? Yes Generating project… Running: pnpm dlx create-vue ┌ Vue.js - The Progressive JavaScript Framework │ ◇ Project name (target directory): │ vue-viteplus │ ◇ Use TypeScript? │ Yes │ ◇ Select features to include in your project: │ Vitest (unit testing), Linter (error prevention), Prettier (code formatting) │ ◇ Select experimental features to include in your project: │ Replace Prettier with Oxfmt │ ◇ Skip all example code and start with a blank Vue project? │ No Scaffolding project in /Users/yanai/project/vue-viteplus... │ └ Done. ✔ Merged vue-viteplus/.oxlintrc.json into vue-viteplus/vite.config.ts ✔ Merged vue-viteplus/.oxfmtrc.json into vue-viteplus/vite.config.ts Wrote agent instructions to CLAUDE.md Rewrote imports in 4 files ✔ Merged staged config into vue-viteplus/vite.config.ts ◇ Dependencies installed ◇ Code formatted ◇ Scaffolded vue-viteplus 出力を見ると Running: pnpm dlx create-vue とあり、内部で create-vue を呼んでいることが分かります。create-vue でプロジェクトを生成した後に、Vite+ が以下の変換をかけています。 .oxlintrc.json → vite.config.ts の lint ブロックにマージ .oxfmtrc.json → vite.config.ts の fmt ブロックにマージ vite / vitest の import パスを vite-plus に書き換え pre-commit フック( vp staged )の設定を統合 つまり vp create vue は create-vue のラッパーで、生成物を Vite+ 向けに変換しているだけのようです。 従来の create-vue pnpm create vue@latest vue-create-vue ┌ Vue.js - The Progressive JavaScript Framework │ ◇ Use TypeScript? │ Yes │ ◇ Select features to include in your project: │ Vitest (unit testing), Linter (error prevention), Prettier (code formatting) │ ◇ Select experimental features to include in your project: │ none │ ◇ Skip all example code and start with a blank Vue project? │ No Scaffolding project in /Users/yanai/project/vue-create-vue... │ └ Done. こちらは従来通りのシンプルな Scaffold です。create-vue 側でも「Replace Prettier with Oxfmt」の選択肢が出ますが、今回は Oxfmt との比較のため Prettier を選びました。 生成される設定ファイルの比較 以降のコードブロックは主要部分の抜粋です。 Vite+ プロジェクトの構成 package.json { " scripts ": { " dev ": " vp dev ", " build ": " run-p type-check \" build-only {@} \" -- ", " build-only ": " vp build ", " type-check ": " vue-tsc --build ", " test:unit ": " vp test ", " lint ": " run-s lint:* ", " lint:oxlint ": " vp lint . --fix ", " lint:eslint ": " eslint . --fix --cache ", " format ": " vp fmt src/ " } , " devDependencies ": { " eslint ": " ^10.1.0 ", " eslint-plugin-vue ": " ~10.8.0 ", " eslint-plugin-oxlint ": " ~1.57.0 ", " eslint-config-prettier ": " ^10.1.8 ", " vite ": " catalog: ", " vite-plus ": " catalog: ", " vitest ": " catalog: " } } vite.config.ts: import { defineConfig } from "vite-plus" import vue from "@vitejs/plugin-vue" export default defineConfig( { staged : { "*" : "vp check --fix" , } , fmt : { semi : false , singleQuote : true , } , lint : { plugins : [ "eslint" , "typescript" , "unicorn" , "oxc" , "vue" , "vitest" ] , env : { browser : true } , categories : { correctness : "error" } , options : { typeAware : true , typeCheck : true } , } , plugins : [ vue() ] , } ) defineConfig を 'vite-plus' からインポートしていて、Vite の設定に加え lint (Oxlint)、 fmt (Oxfmt)、 staged (pre-commit フック)の設定が1つのファイルにまとまっています。 vite と vitest は、 pnpm-workspace.yaml の catalog: により @voidzero-dev のものに解決されています。 従来の create-vue プロジェクトの構成 package.json { " scripts ": { " dev ": " vite ", " build ": " run-p type-check \" build-only {@} \" -- ", " build-only ": " vite build ", " type-check ": " vue-tsc --build ", " test:unit ": " vitest ", " lint ": " run-s lint:* ", " lint:oxlint ": " oxlint . --fix ", " lint:eslint ": " eslint . --fix --cache ", " format ": " prettier --write --experimental-cli src/ " } , " devDependencies ": { " eslint ": " ^10.1.0 ", " eslint-plugin-vue ": " ~10.8.0 ", " eslint-plugin-oxlint ": " ~1.57.0 ", " eslint-config-prettier ": " ^10.1.8 ", " oxlint ": " ~1.57.0 ", " prettier ": " 3.8.1 ", " vite ": " ^8.0.3 ", " vitest ": " ^4.1.2 " } } .oxlintrc.json: { " plugins ": [ " eslint ", " typescript ", " unicorn ", " oxc ", " vue ", " vitest " ] , " env ": { " browser ": true } , " categories ": { " correctness ": " error " } } .prettierrc.json: { " $schema ": " https://json.schemastore.org/prettierrc ", " semi ": false , " singleQuote ": true , " printWidth ": 100 } 共通: eslint.config.ts 前述の通り vp create vue は内部で create-vue を実行しているため、eslint.config.ts は両プロジェクトで同一です。 import { defineConfigWithVueTs, vueTsConfigs, } from "@vue/eslint-config-typescript" import pluginVue from "eslint-plugin-vue" import pluginVitest from "@vitest/eslint-plugin" import pluginOxlint from "eslint-plugin-oxlint" import skipFormatting from "eslint-config-prettier/flat" export default defineConfigWithVueTs( { name : "app/files-to-lint" , files : [ "**/*.{vue,ts,mts,tsx}" ] } , ...pluginVue.configs[ "flat/essential" ], vueTsConfigs.recommended, { ...pluginVitest.configs.recommended, files : [ "src/**/__tests__/*" ] } , ...pluginOxlint.buildFromOxlintConfigFile( ".oxlintrc.json" ), skipFormatting ) ただし、Vite+ プロジェクトではこの eslint.config.ts に落とし穴があります。 Vite+ の公式ガイド では .oxlintrc.json の使用は推奨されておらず、 vite.config.ts の lint ブロックへ設定を集約する方針です。実際、 vp create vue で .oxlintrc.json は vite.config.ts へマージされた後に削除されています。 しかし eslint.config.ts の buildFromOxlintConfigFile(".oxlintrc.json") はそのまま残っています。存在しないファイルを参照すると eslint-plugin-oxlint: could not find oxlint config file: .oxlintrc.json と警告が出て空配列を返すため、ルール重複の無効化が効きません。 つまり、Oxlint と ESLint で同じ違反が重複報告される状態になります。 回避策は2つあります。 1つ目は vite.config.ts から lint ブロックを直接インポートする方法です。 eslint.config.ts は TypeScript ですから、 vite.config.ts の default export から .lint を取り出して buildFromOxlintConfig に渡せます。 // eslint.config.ts import viteConfig from './vite.config' // 変更前: ファイルが存在しないため機能しない ...pluginOxlint.buildFromOxlintConfigFile( ".oxlintrc.json" ), // 変更後: vite.config.ts の lint ブロックをそのまま渡す ...pluginOxlint.buildFromOxlintConfig(viteConfig.lint), 2つ目は vite.config.ts の lint ブロックを削除し、 .oxlintrc.json に設定を一本化する方法です。 vite-plus の issue によると、現状の実装では .oxlintrc.json 等の専用設定ファイルが優先され、 vite.config.ts はフォールバックとして使われます。 .oxlintrc.json があればそちらが読み込まれます。 { " plugins ": [ " eslint ", " typescript ", " unicorn ", " oxc ", " vue ", " vitest " ] , " env ": { " browser ": true } , " categories ": { " correctness ": " error " } , " options ": { " typeAware ": true , " typeCheck ": true } } eslint.config.ts の修正が不要で済みますが、Vite+ の「 vite.config.ts に集約する」方針とは外れます。 共通: Lint 実行順 2026年4月時点で、create-vue は Oxlint をデフォルトで同梱しています。前述の package.json にある通り、 pnpm lint ( run-s lint:* )で lint:oxlint → lint:eslint の順に直列実行されます。 create-vue 側では eslint-plugin-oxlint が .oxlintrc.json を読み取り、Oxlint と重複する ESLint ルールを自動で無効化してくれます。 # Vite+ vp run lint # → vp lint . --fix ... Oxlint(vp経由) # → eslint . --fix --cache ... ESLint(直接呼び出し) # create-vue pnpm lint # → oxlint . --fix ... Oxlint(直接呼び出し) # → eslint . --fix --cache ... ESLint(直接呼び出し) vp lint は Oxlint だけを実行する組み込みコマンドで、ESLint は Vite+ に統合されていません。 そのため、テンプレートでは ESLint を eslint コマンドで直接呼ぶ構成になっています。 Vite+ のタスクランナーを活用したい場合は、 vite.config.ts の run.tasks に定義を移行すると良さそうです。 run.tasks で定義したタスクはデフォルトでキャッシュが有効なため、入力ファイルに変更がなければ再実行がスキップされます。 なお、 run.tasks のタスク名は package.json の scripts と重複できないため、移行する場合は package.json 側の lint スクリプトを削除します。 // package.json の lint 関連スクリプトを run.tasks に移行する例 run: { tasks: { lint: { command: 'vp lint . --fix && eslint . --fix --cache' , input: [{ auto : true } , '!.eslintcache' ] , } , } , } , eslint の --cache を使うと .eslintcache が書き出され、 vp run がそれを入力の変更と見なしてタスクキャッシュがヒットしません。 input で '!.eslintcache' を指定し、キャッシュファイルを変更検知の対象外にすることで併用できます。 キャッシュ機能については ESLint ではなくタスクランナー側のもので十分かもしれませんが、 --cache の有無による差異は今回未検証です。 Linter 比較: Oxlint vs ESLint 検証用コンポーネント 検証用に、意図的に Lint 違反を仕込んだ Vue コンポーネントを用意しました。 < script setup lang = "ts" > import { ref } from "vue" // unused expression (correctness) const x = 1 x // prefer-as-const (typescript) let y = "hello" as "hello" const items = ref ([ { id : 1 , name : "Apple" } , { id : 2 , name : "Banana" } , ]) </ script > < template > <!-- v-for without :key --> < li v- for = "item in items" > {{ item.name }} </ li > <!-- v-if and v-for on same element --> < div v- for = "item in items" v-if= "item.id > 0" :key= "item.id" > {{ item.name }} </ div > </ template > < style scoped> .unused-class { color : redd; } </ style > Oxlint の結果 # Vite+ vp lint src/components/LintTest.vue x eslint(no-unused-expressions): Expected expression to be used , - [src/components/LintTest.vue: 6 : 1 ] 5 | const x = 1 6 | x : ^ `---- x typescript-eslint(prefer-as-const): Expected a ` const ` assertion instead of a literal type annotation. , - [src/components/LintTest.vue: 9 : 20 ] 8 | // prefer-as-const (typescript) 9 | let y = 'hello' as 'hello' : ^^^^^^^ ` ---- Found 0 warnings and 2 errors. Finished in 369ms on 1 file with 132 rules using 10 threads. # create-vue pnpm exec oxlint -c .oxlintrc.json src/components/LintTest.vue ...(同一の 2 件) Found 0 warnings and 2 errors. Finished in 24ms on 1 file with 116 rules using 10 threads. Oxlint は <script> 内の違反を検出しましたが、 <template> / <style> の問題はスルーされています。Oxlint は .vue ファイルの <script> ブロックしか Lint しないためです。 oxc の互換性ページ にもある通り、Vue/Svelte/Astro 等のフレームワークでは script ブロックのみが対象です。 vue プラグインを有効にしても、script ブロック内の Vue 関連ルール(ref の使い方など)しか動きません。 SFC テンプレートの Lint 対応は oxc#15761 で追跡されていますが、まだ実装されていません。 また、Oxlint には ESLint の JS プラグインを読み込む JS plugins 機能もありますが、eslint-plugin-vue は動きません。 JS plugins の 制限事項 に「Custom file formats and parsers (e.g. Svelte, Vue, Angular)」は未対応と明記されています。 eslint-plugin-vue はカスタムパーサー( vue-eslint-parser )で .vue ファイル全体をパースしてテンプレートの AST をルールに渡す仕組みのため、この制限に該当しています。 ルール数の差(132 vs 116)は、 typeAware: true で型情報を使ったチェック(floating promise の検出等)が追加されるためです。 なお、Vite+ テンプレートの typeCheck: true は Vue プロジェクトでは実質的に使えないようです。 vp lint src/ のようにディレクトリを指定すると .ts ファイルもチェック対象になります。 しかし、 .ts から .vue をインポートしている箇所で tsgo がモジュール解決に失敗し TS2307: Cannot find module エラーが出ます。 上の検証でファイルを直接指定しているのはこの問題を回避するためです。 ESLint の結果 # Vite+(eslint-plugin-oxlint が機能していない) vp exec eslint src/components/LintTest.vue src/components/LintTest.vue 6 : 1 error Expected an assignment or function call and instead saw an expression @typescript-eslint/no-unused-expressions 9 : 5 error 'y' is never reassigned. Use 'const' instead prefer-const 9 : 5 error 'y' is assigned a value but never used @typescript-eslint/no-unused-vars 9 : 20 error Expected a `const` instead of a literal type assertion @typescript-eslint/prefer-as-const 19 : 3 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key 22 : 30 error The 'items' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for ✖ 6 problems ( 6 errors, 0 warnings) # create-vue(eslint-plugin-oxlint が正常動作) pnpm exec eslint src/components/LintTest.vue src/components/LintTest.vue 9 : 5 error 'y' is never reassigned. Use 'const' instead prefer-const 9 : 5 error 'y' is assigned a value but never used @typescript-eslint/no-unused-vars 19 : 3 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key 22 : 30 error The 'items' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for ✖ 4 problems ( 4 errors, 0 warnings) Vite+ 側は Oxlint で検出されている no-unused-expressions と prefer-as-const も ESLint から報告されて6件。前述の通りルール重複の無効化が効いていません。 create-vue 側は eslint-plugin-oxlint が正常動作し、Oxlint との重複ルールが ESLint 側で無効化されるため4件。 どちらも、 eslint-plugin-vue により <template> 内の Vue 固有の問題も検出されています。 CSS/Style Lint について 検証用コンポーネントの <style> に color: redd; というタイポを仕込みましたが、Oxlint でも ESLint でも引っかかりませんでした。 どちらのテンプレートも CSS の Lint は対象外のようです。 Vue 公式のツーリングガイドの Linting セクション でも案内されているのは eslint-plugin-vue による JavaScript/テンプレートの Lint だけで、CSS/Style の Lint には触れていません。 CSS の Lint が必要なら、これまで同様 Stylelint と Vue プラグインを別途入れることになりそうです。 型チェックの違い pnpm type-check はどちらも vue-tsc --build で同じです。 Vite+ 側は vite.config.ts に typeAware: true (型認識 Lint ルールの有効化)と typeCheck: true (tsgo 経由の型チェック同時実行)の設定があります。 ただし前述の通り tsgo は .vue を読めないため、 .vue の型チェックには引き続き vue-tsc が必要です。 テスト どちらも Vitest です。 Vite+ ではインポートパスが 'vitest' から 'vite-plus/test' に、コマンドが vitest から vp test に変わりますが、設定内容やテストの書き方は同じです。 Formatter 比較: Oxfmt vs Prettier Oxfmt は Prettier との出力互換を謳っており、JavaScript/TypeScript の conformance test を 100% パスしています。 Vue SFC のフォーマット対応 <template> と <style> もフォーマットできるのか気になったため、わざと崩した Vue ファイルで試しました。 <!-- フォーマット前 --> < template >< div class = "foo" >< p v-if= "true" > hello </ p ></ div ></ template > < style scoped>.foo{ color : red ; font-size : 16px ; display : flex ; justify-content : center }</ style > <!-- フォーマット後(Oxfmt / Prettier どちらも同一の結果) --> < template > < div class = "foo" >< p v-if= "true" > hello </ p ></ div > </ template > < style scoped> .foo { color : red ; font-size : 16px ; display : flex ; justify-content : center ; } </ style > Oxfmt でも Prettier でも <template> と <style> をフォーマットでき、出力結果は同一でした。乗り換えて問題なさそうです。 公式ドキュメント によると Prettier の約30倍の速度とのこと。小規模プロジェクトだと体感差はありませんが、大規模プロジェクトでは差が出そうです。 全体比較表 項目 Vite+ ( vp create vue ) create-vue ( pnpm create vue@latest ) Linter Oxlint ( vp lint ) + ESLint Oxlint + ESLint ESLint 設定 同一 (ただし eslint-plugin-oxlint の修正が必要) 同一 Vue template lint ESLint 経由で対応 ESLint 経由で対応 CSS lint なし なし typeCheck tsgo が .vue を読めず実質使えない なし テスト Vitest ( vp test ) Vitest Formatter Oxfmt ( vp fmt ) Prettier (Oxfmtも案内される) ビルド Vite ( vp build ) Vite ( vite build ) 設定の統合 vite.config.ts に集約 個別ファイル ( .oxlintrc.json , .prettierrc.json ) Pre-commit フック .vite-hooks/pre-commit → vp staged なし (要別途設定) まとめ vp create vue と pnpm create vue@latest で生成されるプロジェクトを比較した結果をまとめます。 Linter Oxlint は .vue の <script> ブロックしか Lint できない <template> や <style> は対象外 Vue template の Lint には引き続き ESLint(eslint-plugin-vue)が必要 これは Vite+ でも create-vue でも同じ create-vue は Oxlint をデフォルトで同梱している ESLint 連携の落とし穴 Vite+ テンプレートでは .oxlintrc.json がマージ後に削除されることへの対応が無く、ルール重複の無効化が壊れる vite.config.ts の lint ブロックを import するか、 .oxlintrc.json に一本化することで対処できる Lint 実行 どちらも Oxlint → ESLint の直列実行 型チェック typeCheck: true は Vue プロジェクトでは実質使えない tsgo が .vue をインポートした .ts ファイルで TS2307 エラーを出すため .vue の型チェックには引き続き vue-tsc が必要 Formatter Oxfmt は Prettier と同一の出力 <template> / <style> もフォーマットできる Prettier からの乗り換えで困ることはなさそう CSS Lint どちらのテンプレートも対象外 必要なら Stylelint を別途入れることになる Vite+ を選ぶメリットは Linter/Formatter 単体の差分よりも、 vite.config.ts への設定一元化と vp コマンドによる統合にありそうでした。 Vue 固有の Lint や型チェックについてはまだ ESLint + vue-tsc 頼りなため、Oxlint の Vue テンプレート対応や tsgo の .vue サポートが整えば、設定が楽になりそうです。 今回はテンプレートの設定比較だけでしたが、気になるのはやはり実際のプロジェクトでの速度差です。 次回は Vue ファイルが約2000個存在する駅メモ!のフロントエンドで、Lint/Format/ビルドがどれくらい速くなるか実測してみます。お楽しみに!
ゼロからの開発で欠かせない「外部ライブラリ」と「ファイル構造」の理解に焦点を当て、Vue.jsプロジェクト作成の手順に沿って解説します。Vue.jsとNode.jsの役割の違い、npm、Vite、Vue CLIといった主要なツールについて説明し、Viteで作成されたプロジェクトの基本的なファイル構成とその役割を具体的に示します。開発の土台となる知識を固めることの重要性を強調します。

動画

該当するコンテンツが見つかりませんでした

書籍