カテゴリー
 SSRでも安心!Angular ResolverからWeb Workerを呼び出してパフォーマンスを改善する方法  
※ 当ページには【広告/PR】を含む場合があります。
2025/10/14

Angularアプリケーションにおいて、ページの表示前にデータを取得・加工するResolverは非常に便利な機能です。しかし、Resolver内で重い処理を実行すると、UIスレッドがブロックされ、ユーザー体験を損なう可能性があります。特にサーバーサイドレンダリング(SSR)を組み合わせた環境では、この問題はより顕著になります。
今回は、この課題を解決するために
ネット上ではComponentやServiceからWorkerを利用する方法は多く紹介されていますが、Resolverからの利用例はまだ少ないようです。この記事が、よりスムーズなユーザー体験を提供する一助となれば幸いです。
Web Workerとは?
まず、Web Workerについて簡単におさらいしましょう。
Web Workerは、ブラウザのメインスレッドとは別のバックグラウンドスレッドでスクリプトを実行するための仕組みです。
通常、JavaScriptはシングルスレッドで動作するため、CPU負荷の高い処理(例えば、大規模なデータセットの計算や画像処理など)を行うと、UIの描画がブロックされ、画面が固まってしまうことがあります。Web Workerは、このような重い処理を別のスレッドに任せることで、メインスレッドの応答性を維持し、ユーザー体験を向上させることを可能にします。メインスレッドとワーカースレッドは
postMessage()onmessageAngularプロジェクトにWeb Workerを追加する
それでは、実際にAngularプロジェクトにWeb Workerを追加していきましょう。Angular CLIには、Web Workerのボイラープレートを生成するための便利なコマンドが用意されています。
            $ ng generate web-worker workers
CREATE src/app/workers.worker.ts (157 bytes)
CREATE tsconfig.worker.json (334 bytes)
UPDATE angular.json (3405 bytes)
        このコマンドを実行すると、以下のファイルが生成・更新されます。
src/app/workers.worker.ts: Workerスクリプト本体です。ここにバックグラウンドで実行したい処理を記述します。 tsconfig.worker.json: Workerをビルドするための専用のTypeScript設定ファイルです。 angular.json: build設定に webWorkerTsConfigプロパティが追加され、Workerのビルド設定がプロジェクトに統合されます。 
            //...
"architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            //👇追加されている
            "webWorkerTsConfig": "tsconfig.worker.json"
          },
//...
        注意点として、このCLIコマンドはプロジェクトタイプが
applicationlibrary            $ ng generate web-worker workers
Web Worker requires a project type of "application".
        ResolverからWorkerを呼び出す実装
今回は、ブログ記事ページのデータをResolverで解決する過程で、何らかの重いデータ加工処理をWorkerに任せる、というシナリオを想定して実装を進めます。
1. Resolverを作成する
まず、Angular CLIでResolverを生成します。ここでは
blogpage.resolver.ts            import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { of, lastValueFrom } from 'rxjs';
import { HttpClient } from '@angular/common/http'; // データ取得用
// ☆Resolverクラスの外部スコープでWorkerのハンドル関数を定義することが重要
async function createWorker(message: any): Promise<any> {
    if (typeof Worker !== 'undefined') {
        // ブラウザ環境ではWorkerを作成して処理を実行
        return new Promise((resolve, reject) => {
            const worker = new Worker(new URL('./workers.worker', import.meta.url));
            worker.onmessage = ({ data }) => {
                worker.terminate();
                resolve(data);
            };
            worker.onerror = (error) => {
                worker.terminate();
                reject(error);
            };
            worker.postMessage(message);
        });
    } else {
        // サーバーサイド(Node.js)環境ではWorkerは使えないため、ここでは何もしない
        // 必要であれば、サーバーサイド用の代替処理をここに記述することも可能
        return Promise.resolve(message);
    }
}
@Injectable({
    providedIn: 'root'
})
export class BlogpageResolver implements Resolve<any> {
    constructor(private http: HttpClient) {}
    async resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Promise<any> {
        try {
            // 例として、外部からブログ記事データを取得
            const _data = await lastValueFrom(this.http.get(`api/articles/${route.paramMap.get('aid')}`));
            // Workerに重いデータ処理を依頼
            const _res = await createWorker(_data);
            // 解決したデータをObservableとしてルートに渡す
            return of(_res);
        } catch (error) {
            console.error('Resolver Error:', error);
            // エラーが発生した場合は空のObservableを返すなど、適切なエラーハンドリングを行う
            return of(null);
        }
    }
}
        ここで重要なポイントは、
typeof Worker !== 'undefined'2. ルーティング設定にResolverを追加する
次に、作成したResolverをルーティング定義に追加します。これにより、特定のルートにアクセスした際に
BlogpageResolver            import { Routes } from '@angular/router';
import { BlogPageComponent } from './blog-page.component';
import { BlogpageResolver } from './blogpage.resolver';
export const blogRoutes: Routes = [
    {
        path: 'blog/:aid',
        component: BlogPageComponent,
        resolve: {
            // 'blogpage'というキーでResolverの解決結果をマッピング
            blogpage: BlogpageResolver,
        }
    }
];
        3. Workerスクリプトを編集する
次に、Worker本体である
workers.worker.ts            /// <reference lib="webworker" />
import '@angular/compiler';
// 例として、重い処理を行う外部ライブラリをインポート
import { someHeavyProcess } from 'some-heavy-library';
addEventListener('message', ({ data }) => {
    // メインスレッドから渡されたデータを使って重い処理を実行
    const result = someHeavyProcess(data);
    // 処理結果をメインスレッドに返す
    postMessage(result);
});
        ここで一つ注意点があります。Worker内でAngularの機能や外部ライブラリを利用しようとすると、JITコンパイルに関するエラーが発生することがあります。
            Uncaught Error: The injectable 'PlatformLocation' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available.
        このようなエラーが発生した場合は、Workerファイルの先頭で
import '@angular/compiler';4. Componentで解決済みのデータを受け取る
最後に、ページのレンダリングを担当するComponent側で、Resolverが解決したデータを
ActivatedRoute            import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { Observable } from 'rxjs';
@Component({
    selector: 'app-blog-page',
    standalone: true,
    imports: [CommonModule],
    template: `
        <div *ngIf="isLoading" class="loader">Loading...</div>
        <article *ngIf="articleData$ | async as article">
            <h1>{{ article.title }}</h1>
            <div [innerHTML]="article.content"></div>
        </article>
    `,
})
export class BlogPageComponent implements OnInit {
    isLoading: boolean = true;
    articleData$!: Observable<any>;
    private route = inject(ActivatedRoute);
    ngOnInit() {
        // route.dataはObservable<Data>を返す
        this.route.data.subscribe({
            next: (data) => {
                // 'blogpage'キーで登録したResolverの結果(Observable)を取得
                if (data && data['blogpage']) {
                    this.articleData$ = data['blogpage'];
                    this.isLoading = false;
                }
            },
            error: (err) => {
                console.error(err.message);
                this.isLoading = false;
            }
        });
    }
}
        ActivatedRoutedataObservabledatasubscribeblogpageObservableasyncまとめ
いかがでしたでしょうか。今回はAngularのResolverからWeb Workerを利用して、重い処理をバックグラウンドに逃がす方法について解説しました。
UIスレッドの解放 : Resolver内の重い処理をWeb Workerにオフロードすることで、UIスレッドのブロッキングを防ぎ、アプリケーションの応答性を維持できます。 環境の判定 : typeof Worker !== 'undefined'を使い、ブラウザ環境とサーバーサイド(SSR)環境で処理を振り分けることが重要です。 CLIの活用 : Angular CLIの ng generate web-workerコマンドで、Worker関連のファイルを簡単に追加・設定できます。 データ連携 : Resolverは ActivatedRouteの dataプロパティを通じて、解決したデータをComponentに効率的に渡すことができます。 
この手法を活用することで、特にデータ処理に時間がかかるようなページでも、ユーザーにストレスを与えることなくスムーズなナビゲーションを提供できるはずです。ぜひ、お手元のAngularプロジェクトで試してみてください。