Angular/SSRでブラウザにあってNodejsにないJavascript APIクラスを使う際に気を使うこと


※ 当ページには【広告/PR】を含む場合があります。
2025/07/09
Angular/SSRアプリケーションでPrisma/SQLiteを利用する際の注意点
蛸壺の技術ブログ|Angular/SSRでブラウザにあってNodejsにないWebAPIクラスを使う際に気を使うこと

Angular/SSRを使っているとサーバー側で、Nodejsの組み込みとしては使えないWebAPIの関数が入っていると、

            ERROR ReferenceError: Image is not defined
    at e.ngOnInit (file:///var/task/lambda.mjs:1086:1059)
#...
        
とか、

            ERROR ReferenceError: IntersectionObserver is not defined
    at e._subscribe (file:///var/task/lambda.mjs:1073:3367)
#...
        
みたいなエラーが起こることがあります。

サーバー側で起こることなので、クライアント側のブラウザ上ではエラーとしては認識されることはないため、深刻に捉えることがなければ開発者としては割と対応を後回しになることが多いかもしれません。

ただし、Angularの試験的な機能として、「インクリメンタルハイドレーション」など、新しいレンダリングの仕組みを今後使うシーンも多くなってきそうな手前、クライアント側のレンダリング結果に影響を及ぼす可能性も大きくなってきます。

ブラウザ標準のWeb APIも色々とあるので、とても全ては網羅できませんが、個人的な凡例として以下2つの実装例で対処法を説明しておきましょう。


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

実装例①〜Imageクラスをサーバーで動作を防止する

おそらくよくあるのはImageクラスで画像のサイズ取得を自動化させる処理です。

Imageクラスは
HTMLImageElementのコンストラクタ関数であるため、通常、Nodejs内では処理不可能です。

実装方針の一例としては、以下のようにすると良いでしょう。

            import {
    Component, OnInit,
} from '@angular/core';
import { SafeResourceUrl } from '@angular/platform-browser';

@Component({
    selector: 'app-imgsrc',
    template: `
        <img [src]="imgsrc1"
            [width]="imgWidth || 100"
            [height]="imgHeight || 100"/>
    `,
    standalone: true,
})
export class ImgsrcComponent implements OnInit {
    imgsrc1: SafeResourceUrl = '';
    imgWidth: number| undefined;
    imgHeight: number| undefined;

    ngOnInit() {
        const _imgpath = /* 画像のURL */;
        this.imgsrc1 = _imgpath;

        //👇サーバーではImageクラスが無いので、ブラウザ側のみで処理される
                if (typeof Image !== 'undefined') {
                    const img = new Image();
                    img.onload = () => {
                        const size: {
                            width: number;
                            height: number;
                        } = {
                            width: img.naturalWidth,
                            height: img.naturalHeight,
                        };
                        this.imgWidth = size.width;
                        this.imgHeight = size.height;
                    };
                    img.src = _imgpath;
                }
    }
    //...
}
        
重要なのはシンプルにtypeof Imageを呼び出して、呼び出し元のランタイムにオブジェクトの中身があるかないかを判定しているだけです。

少し前には、クライアント・サーバーのロジックを分離するために、以下のようなテクニックを良く使っていました。

            import { inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

//...

platformId = inject(PLATFORM_ID);

//...

if (isPlatformBrowser(this.platformId)) {

    //...ブラウザ側だけの処理

} else {

    //...サーバー側だけの処理

}

//...
        

現行の
@angular/ssrでこのパターンを使うと、「No hydration info in server response」エラーが起こる可能性があります。

参考|NG05050

このエラーはSSRする際に、クライアント側のレンダリングのロジックとサーバー側のレンダリングのロジックに食い違いがあることで引き起こされます。

現在は自前でクライアントとサーバーのロジックは分離せずに、全てAngularのビルダーを信頼して自動で行わせることが重要です。


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

実装例②〜IntersectionObserverクラスをサーバーで動作を防止する

もう一つはDOM要素のLazy Loadingのカスタマイズに便利なIntersectionObserverクラスの例も挙げておきます。

以下のクラスは、任意のDOM要素がブラウザの視界に入った際に処理が開始されるようなDIサービスになります。

            import { inject, Injectable, ElementRef } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import {
    Observable, fromEvent, defer, concat, combineLatest,
    map, mergeMap, distinctUntilChanged,
} from 'rxjs';

@Injectable({
    providedIn: 'any'
})
export class VisibilityService {
    document: Document = inject(DOCUMENT);
    private pageVisible$: Observable<boolean>;

    constructor() {
        this.pageVisible$ = concat(
            defer(() => of(!this.document.hidden)),
            fromEvent(this.document, 'visibilitychange').pipe(
                map(e => !this.document.hidden),
            )
        );
    }

    elementInSight(element: ElementRef | Element): Observable<boolean> | undefined {
        //👇サーバー側ではIntersectionObserverが無いため、ブラウザのみで動作する
        if (typeof IntersectionObserver === 'undefined') {
            return undefined;
        }

            const elementVisible$ = new Observable<IntersectionObserverEntry[]>(observer => {
                const intersectionObserver = new IntersectionObserver(entries => {
                    observer.next(entries);
                });
                if (element instanceof ElementRef) {
                    intersectionObserver.observe(element.nativeElement);
                }
                else if (element instanceof Element) {
                    intersectionObserver.observe(element);
                }
                return () => { intersectionObserver.disconnect(); };
            }).pipe (
                mergeMap((entries: IntersectionObserverEntry[]) => entries),
                map((entry: IntersectionObserverEntry) => entry.isIntersecting),
                distinctUntilChanged()
            );
            return combineLatest([this.pageVisible$, elementVisible$]).pipe (
                map((flags) => flags[0] && flags[1]),
                distinctUntilChanged()
            );
    }
}
        
見てのように、こちらもtypeof IntersectionObserverで処理が走っている環境にクラスの実体があるかないかで判別しています。

これで呼び出し側の実装はクライアントかサーバーかのことは意識せずにコードに組み込むことができます。

            import {
    Component, OnInit, inject,
    ElementRef, OnDestroy, ViewChild,
} from '@angular/core';
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';

import {
    Subscription,
    of, filter, take,
    mergeMap
} from 'rxjs';

//👇先程のサービス
import { VisibilityService } from '../service';

@Component({
    selector: 'app-imgsrc',
    template: `
    <div #img_canvas>
        <img [src]="imgsrc1"
            [width]="imgWidth || 100"
            [height]="imgHeight || 100"/>
    </div>
    `,
    standalone: true,
})
export class ImgsrcComponent implements OnInit, OnDestroy {
    @ViewChild('img_canvas', {static: true}) wrapper: ElementRef | undefined;

    imgsrc1: SafeResourceUrl = '';
    imgWidth: number| undefined;
    imgHeight: number| undefined;

    inSight$: Subscription| undefined;

    private domSanitizer = inject(DomSanitizer);
    private visibility = inject(VisibilityService);

    ngOnInit() {
        const _imgpath = /* 画像のURL */;

                //👇ブラウザ側のみで動作する
                this.inSight$ = this.visibility.elementInSight(this.wrapper!)?.pipe(
                    filter(visible => visible),
                    take(1),
                    mergeMap(_ => of(_imgpath))
                ).subscribe((url: string) => {
                    this.imgsrc1 = this.domSanitizer.bypassSecurityTrustResourceUrl(url);
                    const img = new Image();
                    img.onload = () => {
                        const size: {
                            width: number;
                            height: number;
                        } = {
                            width: img.naturalWidth,
                            height: img.naturalHeight,
                        };
                        this.imgWidth = size.width;
                        this.imgHeight = size.height;
                    };
                    img.src = url;
                });
    }

    ngOnDestroy() {
        if (this.inSight$) {this.inSight$.unsubscribe();}
    }
}
        
これでサーバー側に要素の数だけ大量にエラーが起こるような症状はなくなっているはずです。


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

まとめ

今回はAngular/SSRで地味に頻発するエラーの対処法を紹介しました。

ブラウザ(標準のJavascript API)とサーバー(Nodejs)の間で起こる処理の食い違いによるエラーならば、おおよそ今回のテクニックの応用で対処できるかと思います。

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

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