SSRでも安心!Angular ResolverからWeb Workerを呼び出してパフォーマンスを改善する方法


※ 当ページには【広告/PR】を含む場合があります。
2025/10/14
【Angular/SSRでSEO対策】Angular/SSRサイトのURL変更時に役立つ301リダイレクト設定
蛸壺の技術ブログ|SSRでも安心!Angular ResolverからWeb Workerを呼び出してパフォーマンスを改善する方法


Angularアプリケーションにおいて、ページの表示前にデータを取得・加工するResolverは非常に便利な機能です。しかし、Resolver内で重い処理を実行すると、UIスレッドがブロックされ、ユーザー体験を損なう可能性があります。特にサーバーサイドレンダリング(SSR)を組み合わせた環境では、この問題はより顕著になります。
今回は、この課題を解決するために
AngularのResolverからWeb Workerを呼び出す方法 について解説します。Web Workerを利用することで、時間のかかる処理をバックグラウンドのスレッドにオフロードし、メインスレッドを解放することができます。
ネット上ではComponentやServiceからWorkerを利用する方法は多く紹介されていますが、Resolverからの利用例はまだ少ないようです。この記事が、よりスムーズなユーザー体験を提供する一助となれば幸いです。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

Web Workerとは?



まず、Web Workerについて簡単におさらいしましょう。
Web Workerは、ブラウザのメインスレッドとは別のバックグラウンドスレッドでスクリプトを実行するための仕組みです。
通常、JavaScriptはシングルスレッドで動作するため、CPU負荷の高い処理(例えば、大規模なデータセットの計算や画像処理など)を行うと、UIの描画がブロックされ、画面が固まってしまうことがあります。Web Workerは、このような重い処理を別のスレッドに任せることで、メインスレッドの応答性を維持し、ユーザー体験を向上させることを可能にします。メインスレッドとワーカースレッドは
postMessage() メソッドと onmessage イベントハンドラを介して互いに通信します。これにより、安全にデータのやり取りが行えるというわけです。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

Angularプロジェクトに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コマンドはプロジェクトタイプが
application の場合にのみ利用可能です。 library タイプのプロジェクトでは、Worker APIがブラウザ環境に依存するため、以下のようなエラーが発生します。

            $ ng generate web-worker workers
Web Worker requires a project type of "application".

        

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

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);
        }
    }
}

        

ここで重要なポイントは、
Workerをインスタンス化して通信するロジックを、Resolverクラスの外部スコープに非同期関数として定義している点 です。これにより、ブラウザ環境とサーバー環境(SSR)での動作を条件分岐でハンドリングしやすくなります。 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'; をインポートすることで、WorkerのコンテキストにAngularコンパイラを読み込ませ、問題を回避できる場合があります。

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;
            }
        });
    }
}

        
ActivatedRoutedata プロパティは、Resolverから返された値を含む Observable です。この datasubscribe し、ルーティング設定で指定したキー(今回は blogpage )を使ってデータを取り出します。Resolverが Observable を返すため、Component側では async パイプを使ってテンプレート内でスマートにデータを扱うことができます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

まとめ



いかがでしたでしょうか。今回はAngularのResolverからWeb Workerを利用して、重い処理をバックグラウンドに逃がす方法について解説しました。

  • UIスレッドの解放 : Resolver内の重い処理をWeb Workerにオフロードすることで、UIスレッドのブロッキングを防ぎ、アプリケーションの応答性を維持できます。
  • 環境の判定 : typeof Worker !== 'undefined' を使い、ブラウザ環境とサーバーサイド(SSR)環境で処理を振り分けることが重要です。
  • CLIの活用 : Angular CLIの ng generate web-worker コマンドで、Worker関連のファイルを簡単に追加・設定できます。
  • データ連携 : Resolverは ActivatedRoutedata プロパティを通じて、解決したデータをComponentに効率的に渡すことができます。

この手法を活用することで、特にデータ処理に時間がかかるようなページでも、ユーザーにストレスを与えることなくスムーズなナビゲーションを提供できるはずです。ぜひ、お手元のAngularプロジェクトで試してみてください。