Dev Study
← 解説「通信と画像の最適化 — 減らす・まとめる・待たせ方を工夫する」に戻る

サンプルコードで身につける: 通信と画像の最適化 — 減らす・まとめる・待たせ方を工夫する

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1img の src を ngSrc に替える

NgOptimizedImage導入の最小ビフォーアフターです。srcをngSrcに替えてwidthとheightを書くだけで、画面外画像の遅延読み込みとレイアウトずれ防止が手に入ります。サイズ指定を忘れると開発モードでエラーとして教えてくれます。

import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-product-photo',
  imports: [NgOptimizedImage],
  // ── 遅い書き方 ──
  //   '<img src="/assets/product.jpg">'
  //   => すぐ全部読み込まれ、サイズ未指定で表示がガタつく
  //
  // ── 速い書き方: ngSrc + width/height ──
  template:
    '<img ngSrc="/assets/product.jpg" width="600" height="400" />',
})
export class ProductPhotoComponent {}

// ngSrc にすると:
//   ・画面外の画像は近づくまで読み込まれない(loading="lazy" 相当)
//   ・width/height が必須になり、レイアウトのずれを防げる
//   ・書き忘れや誤用は開発モードで具体的なエラーが出る

2priority でファーストビューの主役画像を最優先にする

LCP(最大の表示要素)になるヒーロー画像へのpriority指定の例です。priorityを付けた画像は遅延されず最優先で取得されます。全画像に付けると意味がなくなるため、ファーストビューの主役1〜2枚に限定するのが使い方のコツです。

import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-top-page',
  imports: [NgOptimizedImage],
  template:
    // ファーストビューの主役画像だけ priority(最優先で読み込み)
    '<img ngSrc="/assets/hero.jpg" width="1200" height="600" priority />' +
    // ページ下部の画像は指定なし = スクロールで近づくまで遅延
    '<img ngSrc="/assets/feature-1.jpg" width="600" height="400" />' +
    '<img ngSrc="/assets/feature-2.jpg" width="600" height="400" />',
})
export class TopPageComponent {}

// Lighthouse の LCP の項目に効く。効果は再計測で確認する。
// priority の付けすぎは「全部最優先」= 何も優先していないのと同じ

3shareReplay で同じAPIを何度も呼ばない

複数コンポーネントが同じマスタデータをそれぞれ取得してしまう問題の定番解です。共有サービスにObservableを1本持たせshareReplay(1)を挟むと、最初の1回だけ通信し、以降の購読者にはキャッシュ済みの結果が配られます。

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, shareReplay } from 'rxjs';

type Category = { id: number; name: string };

// ── 遅い書き方: 各コンポーネントが個別に取得 ──
// ヘッダーもサイドバーも検索フォームも、それぞれ
// this.http.get('/api/categories') を呼ぶ => 同じ通信が3回飛ぶ

// ── 速い書き方: サービスに1本だけ持ち、結果を共有する ──
@Injectable({ providedIn: 'root' })
export class CategoryService {
  private http = inject(HttpClient);

  // 最初の購読で1回だけ通信し、結果を全購読者で使い回す
  readonly categories$: Observable<Category[]> = this.http
    .get<Category[]>('/api/categories')
    .pipe(shareReplay(1));
}

// 確認は DevTools の Network タブで。/api/categories が
// 1回しか飛ばなくなれば成功。
// 注意: 更新され得るデータに使うなら「いつ取り直すか」も決めておく

4取得中はスケルトンを見せる

消せない通信の待ち時間を、見せ方で短く感じさせる例です。データ未着のあいだは本物と同じ形の灰色の枠(スケルトン)を出し、届いたら一覧に差し替えます。真っ白な画面やくるくる回るだけの表示より体感が良くなります。

import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

type Article = { id: number; title: string };

@Component({
  selector: 'app-article-list',
  // データ未着(null)のあいだは、本物と同じ形のスケルトンを出す
  template:
    '@if (articles(); as list) {' +
    '  @for (a of list; track a.id) { <p>{{ a.title }}</p> }' +
    '} @else {' +
    '  @for (i of [1, 2, 3]; track i) {' +
    '    <div class="skeleton-row"></div>' +
    '  }' +
    '}',
  // .skeleton-row は「高さを実物に合わせた灰色の帯」にする。
  // 高さが合っていれば、差し替え時に画面がガタつかない
  styles: '.skeleton-row { height: 48px; background: #eee; margin: 8px 0; }',
})
export class ArticleListComponent {
  private http = inject(HttpClient);
  articles = signal<Article[] | null>(null);

  constructor() {
    this.http.get<Article[]>('/api/articles')
      .subscribe((list) => this.articles.set(list));
  }
}

5@defer の @placeholder と @loading で待ち時間を設計する

既習の@deferを「ローディング体験の設計」の道具として使う例です。@placeholderで読み込み前の場所を確保し、@loadingで取得中の表示を出します。minimumを指定すると一瞬だけ表示がチカッと切り替わる不快さを防げます。

import { Component } from '@angular/core';
import { CommentSectionComponent } from './comment-section.component';

@Component({
  selector: 'app-article-page',
  imports: [CommentSectionComponent],
  template:
    '<article>本文...</article>' +
    '@defer (on viewport) {' +
    '  <app-comment-section />' +
    '} @placeholder {' +
    // 読み込み開始前: 高さを確保してガタつきを防ぐ
    '  <div class="comment-skeleton">コメント欄</div>' +
    '} @loading (minimum 300ms) {' +
    // 取得中: minimum を付けると一瞬で消えるチラつきを防げる
    '  <div class="comment-skeleton">コメントを読み込み中...</div>' +
    '} @error {' +
    '  <p>コメントを読み込めませんでした</p>' +
    '}',
  styles: '.comment-skeleton { min-height: 200px; background: #eee; }',
})
export class ArticlePageComponent {}

// placeholder と本体の高さを揃えておくのがコツ。
// 差し替えの瞬間にページ全体がずれない