Step 2: Browser & SPA
12. async/await の役割:変換結果の待機と画面の重さを分けて考える
async/awaitは、Promiseの完了を待ち、結果や失敗を次の処理へ渡すために使います。長い同期計算を別スレッドへ移す指定ではありません。画像変換の本番コードと、愛猫の写真を読み込み・配置・保存する完全サンプルで、「どこまでを待つか」を確かめます。
メインスレッドの長い計算は画面操作を待たせる
サーバー側のマルチスレッド処理に慣れているエンジニアにとって、フロントエンド開発での最大の衝撃は「JavaScriptはシングルスレッドである」という事実かもしれません。ブラウザ上のJavaScriptエンジンは、基本的に一度に一つの作業しかこなせません。
メインスレッドを占有する長い同期計算では、イベント処理や次の画面描画が待たされます。ライブラリがWorkerで計算する場合は実行場所が異なるため、使用版の実装と実際の処理を確認します。
async/awaitで非同期処理の結果を受け取る
完了まで時間がかかる処理の結果を受け取るには、Promise と async/await を使えます。async を付けるだけで画像の計算が別スレッドに移るわけではありません。
await は呼び出した非同期関数の続きだけを一時停止します。通信やWorkerの完了待ちなら、その間にメインスレッドが別の仕事をできます。一方、await の右辺を評価する際に長い同期計算を実行すれば、その計算中はメインスレッドを占有します。
Before/After:コードで見る違い
まず、Promiseを変換済み画像と取り違える例です。heic2any を読み込んだ環境を前提とし、showLoading などはアプリ側で用意するUI関数です。
// 誤り:変換結果ではなくPromiseを表示処理へ渡している
function processImage(file) {
showLoading();
const converted = heic2any({ blob: file }); // 戻り値はPromise
displayImage(converted);
hideLoading();
} 次は、heic2anyのPromiseから変換済みBlobを受け取る説明例です。showLoadingなどのUI関数は省略しているため、完全に動かす場合は後述のサンプルを使ってください。
// 完了した結果を取得し、失敗時も表示状態を戻す例
async function handleImage(file) {
showLoading(); // 実際の画面描画はブラウザの描画タイミングで行われる
try {
// await で変換が終わるのを待つ。
// UIの応答性は変換ライブラリの実行方式にも依存します。
const result = await heic2any({ blob: file, toType: 'image/jpeg' });
const converted = Array.isArray(result) ? result[0] : result;
if (!(converted instanceof Blob)) throw new Error('画像がありません');
displayImage(converted);
} catch (error) {
console.error("変換に失敗しました", error);
} finally {
hideLoading();
}
} 注意点:待機と別スレッドの実行は違う
非同期処理を使えばどんなに重い計算も完璧に解決する、というわけではありません。以下の点には注意が必要です。
- 計算そのものの負荷:
awaitは「待ち時間」を効率化しますが、CPUをフルに使う純粋な計算処理(暗号化や画像加工など)自体がメインスレッドを占有する場合、async/awaitだけではフリーズを防げないことがあります。その場合は Web Workers などの並列処理を検討する必要があります。 - エラーハンドリング:
awaitする処理が失敗したとき、どこでtry/catchなどを使って扱うかを決めましょう。呼び出し元も含めて失敗を扱わないと、未処理のPromise拒否や処理中表示の残留につながります。どの層で利用者へエラーを伝えるか決めておきます。
フリーズの調査は「待ち時間」と「計算時間」を分ける
開発者ツールのPerformance記録でメインスレッドの長い処理を調べます。ピクセルを走査する同期ループが原因なら、入力画像の縮小、計算の分割、Workerへの移動などを検討します。requestAnimationFrame もメインスレッドで実行されるため、重いループをその中へ移すだけでは軽くなりません。
画像をまとめて変換する場合は、Promise.all() で一斉に開始する前にメモリの上限も考えます。1枚ずつ処理する方がピーク使用量を抑えられる場合があります。処理時間と同時に、同じ操作を繰り返したときの使用量を確認するのが画像ツールでは重要です。
本番のawaitと画像コールバックは、待つ範囲が違う
画像変換ツールのhandleFile()は、HEICライブラリの取得と変換をawaitします。その後はimg.onloadとcanvas.toBlob()のコールバックで進みます。img.srcを設定した時点でこのasync関数の末尾に達するので、handleFileをawaitしただけでは後続の画像表示・保存結果まで待つことにはなりません。
const heicModule = await import('heic2any');
const heic2any = heicModule.default || heicModule;
const converted = await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.9 });
// 以降の本番処理はimg.onloadとtoBlobのコールバックへ続く。本番の関数名と待機位置を読み取る抜粋です。制作解説では変換結果をCanvasへ渡します。新しい教材では画像デコードとPNG生成をPromiseで待つようにし、処理終了までの順番を明示します。本番を変更したという意味ではありません。
読み込みとPNG生成が終わるまで待つサンプル
愛猫写真の動作サンプルを開く / 完全サンプルZIPを保存する
ZIPをすべて展開し、index.html・app.js・cat.jpg・README.txtがあるフォルダーでターミナルを開きます。Node.jsが未導入ならNode.js公式サイト(別タブで開く)のLTS版を導入してターミナルを開き直します。次を1行ずつ実行し、サーバーを開いたままにします。
node --version
npm.cmd --version
npx.cmd --yes http-server . -p 8080 -c-1ブラウザでhttp://localhost:8080/を開きます。macOS・Linuxはnpm・npxへ読み替えます。8080が使用中ならコマンドとURLを8081へ変更し、終了はCtrl+Cです。file://では起動しません。初回のHTTPサーバー取得には通信が必要です。
上のサンプルはFile API→Canvas→PNG保存を一つにつなげます。processImage()は入力取得、デコード、配置、PNGのBlob生成の順に進み、最後に保存リンクを有効にします。処理中は入力と実行ボタンを無効にし、失敗してもfinallyで戻します。
const file = await getInputFile();
const dimensions = await readFileIntoCanvas(file);
const grid = drawGrid();
const blob = await encodePng(outputCanvas);
// この時点でPNGのBlobが生成済みなので保存リンクを作れる。drawGrid()は同期処理です。awaitを付けて重い計算を別スレッドにしているわけではありません。一方、encodePng()はtoBlobのコールバックをPromiseへ包むので、呼び出し元は生成後のBlobをawaitで受け取れます。
function encodePng(canvas) {
return new Promise((resolve, reject) => {
try {
canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('PNGを生成できませんでした')), 'image/png');
} catch (error) { reject(error); }
});
}実行後のJSONで入力寸法と生成したBlobを確認します。ファイル選択だけでは新しい結果を作らず、保存リンクをいったん外します。画像の入力はFile APIの章、描画と完全コードはCanvasの記事へ進んでください。
完了したBlobと保存ファイルを照合する
Windows・Chrome 154で実行し、画面が「完了」になった後のPNGは960×720 px、764,899バイトでした。保存リンクから保存したファイルもこの寸法・容量と一致しました。await encodePng()の後に得たBlobを保存へ渡すまで、一連の動作を確認した結果です。処理速度やHEIC変換、スマホ実機を測定した結果ではありません。実行画面と配置の確認結果も見られます。
まとめ:優れたUXには「非同期」の理解が不可欠
画像の取得、デコード、計算、書き出しを区切り、それぞれがPromiseを返すのか、同期処理なのかを確認します。表示を変えても画面描画は別のタイミングなので、処理中の見た目だけで待機方法を判断しません。