angular ssr

Angular SSR: サーバーサイドレンダリングの完全ガイド

このガイドでは、Angularを使用してサーバーサイドレンダリング(SSR)の基本を理解し、実装する方法を紹介します。SSRを利用することで、アプリケーションのパフォーマンスとSEO効果を向上させることが可能です。この記事を通じて、Angularアプリケーションを最適化する方法を学びましょう。

サーバーサイドレンダリングのメリットとは?

サーバーサイドレンダリングは、初回のページロード速度を向上させ、クライアントに返すHTMLを事前に生成することで、ユーザー体験を向上させます。また、SEOの観点からも、検索エンジンがコンテンツを容易にインデックスできるため、オーガニックトラフィックの増加が期待できます。

サーバーサイドレンダリングを実装する方法

AngularアプリケーションでSSRを実現するには、@angular/platform-serverパッケージを使用します。以下に、基本的なセットアップ手順を示します。

1. Angular Universalをインストールします:
   npm install @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader --save

2. サーバーコードを作成します:
   <code>
   import 'zone.js/dist/zone-node';
   import { enableProdMode } from '@angular/core';
   import { ngExpressEngine } from '@nguniversal/express-engine';
   import * as express from 'express';
   import { join } from 'path';
   
   // ターゲットアプリについての情報
   const APP_SERVER_MODULE = require('./dist/YOUR_APP_NAME/server/main').AppServerModuleNgFactory;
   const APP_SERVER_MODULE_MAP = require('./dist/YOUR_APP_NAME/server/main').LAZY_MODULE_MAP;

   enableProdMode();
   const app = express();
   const PORT = process.env.PORT || 4000;
   const DIST_FOLDER = join(process.cwd(), 'dist/YOUR_APP_NAME/browser');

   // Expressエンジンを設定
   app.engine('html', ngExpressEngine({
       bootstrap: APP_SERVER_MODULE,
       providers: [{ provide: LazyModuleMap, useValue: APP_SERVER_MODULE_MAP }]
   }));

   app.set('view engine', 'html');
   app.set('views', DIST_FOLDER);

   // ルートのハンドリング
   app.get('*', (req, res) => {
       res.render('index', { req });
   });

   app.listen(PORT, () => {
       console.log(`Node server listening on http://localhost:${PORT}`);
   });

SSRとSEOの影響

SSRによってHTMLがサーバー上で事前に生成されるため、検索エンジンはページコンテンツを簡単に取得でき、インデックス化の効率が向上します。これにより、関連性の高い検索キーワードに対するオーガニックトラフィックが増加する可能性があります。

メリット 説明
初回読み込み速度 クライアントサイドレンダリングに比べて、ページの初回表示が速くなります。
SEO効果 検索エンジンに対してコンテンツが込められたHTMLを提供できるため、インデックス化が容易になります。
ユーザー体験 迅速なレンダリングにより、ユーザーの満足度が向上することが期待できます。

Angular Universal の変化

Angular v17 から、Angular Universal が Angular CLI に統合されました。これにより、SSR (Server-Side Rendering) アプリケーションの作成プロセスが大幅に簡素化されました。以前は、Angular Universal を使用するには、別途パッケージをインストールし、複雑な設定を行う必要がありましたが、v17 以降は、Angular CLI のコマンド一つで SSR アプリケーションを作成することができます。

Angular 公式の マイグレーションガイド によると、機能とアーキテクチャに変更はなく、コードのリファクタリングと名前の変更が行われただけです。

初期設定

まず、ng new コマンドで SSR アプリケーションを作成します。--ssr オプションを指定することで、SSR アプリケーションとして作成されます。

ng new my-ssr-app --ssr

アプリケーションが作成されたら、npm run build コマンドでアプリケーションをビルドし、npm run serve:ssr コマンドでアプリケーションを実行します。<application name> 部分は作成したアプリケーション名に置き換えてください。

npm run build && npm run serve:ssr:my-ssr-app

アプリケーションが起動したら、ブラウザで http://localhost:4000 にアクセスします。

中身を見てみる

SSR アプリケーションは、SPA (Single-Page Application) アプリケーションと比べて、いくつかのファイルが追加されています。主な違いは以下の通りです。

  • server.ts: SSR を実行するためのサーバーサイドのコード
  • src/main.server.ts: サーバーサイドでアプリケーションをブートストラップするためのコード
  • src/app/app.config.server.ts: サーバーサイドでアプリケーションを設定するためのコード

server.ts ファイルを見てみましょう。このファイルでは、Express フレームワークを使って SSR を実行しています。

// ... (その他インポート)

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/my-ssr-app/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  server.set('view engine', 'html');
  server.set('views',```html
 distFolder);

  // Example Express API endpoints
  // server.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  });

  return server;
}

function run(): void {
  const port = process.env['PORT'] || 4000;

  // Start up the Node server
  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// Call the run function to start up the server
run();

server.ts は、Express アプリケーションを作成し、Angular Universal の ngExpressEngine を使って Angular アプリケーションをレンダリングしています。また、静的ファイルの配信や API エンドポイントの設定なども行っています。

Lambda に載せてみる

それでは、この SSR アプリケーションを Lambda にデプロイしてみましょう。今回は、Serverless Framework を使ってデプロイします。

Serverless Framework の設定

まず、Serverless Framework と関連するライブラリをインストールします。

npm install serverless -g
npm install @serverless/typescript
npm install @h4ad/serverless-adapter

次に、serverless.ts ファイルを作成し、以下の設定を記述します。

import type { AWS } from '@serverless/typescript';

const serverlessConfig: AWS = {
  service: 'angular-ssr-lambda',
  frameworkVersion: '3',
  configValidationMode: 'error',
  plugins: ['serverless-esbuild', '@serverless/typescript'],
  provider: {
    name: 'aws',
    runtime: 'nodejs18.x',
    region: 'ap-northeast-1',
    apiGateway: {
      minimumCompressionSize: 1024,
      shouldStartNameWithService: true,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
      NODE_OPTIONS: '--enable-source-maps --stack-trace-limit=1000',
    },
  },
  functions: {
    ssr: {
      handler: 'handler.handler',
      url: true,
      package: {
        patterns: [
          '!node_modules/**',
          'dist/my-ssr-app/browser/**',
        ]
      }
    }
  },
  custom: {
    esbuild: {
      bundle: true,
      minify: false,
      sourcemap: true,
      exclude: ['aws-sdk'],
      target: 'node18',
      define: { 'require.resolve': undefined },
      platform: 'node',
      concurrency: 10,
    },
  },
};

module.exports = serverlessConfig;

設定の説明:

  • handler: Lambda 関数のエントリポイントを指定します。ここでは、handler.ts ファイルの handler 関数を指定しています。
  • url: Lambda Function URL を有効にするかどうかを指定します。true にすると、Lambda 関数に URL が割り当てられ、ブラウザからアクセスできるようになります。
  • package: デプロイするファイルのパターンを指定します。ここでは、node_modules フォルダを除外し、dist/my-ssr-app/browser フォルダ以下のファイルをデプロイするように設定しています。

Express インスタンス作成部を分離する

Lambda 環境では、server.ts のように Express インスタンスを作成する部分を分離する必要があります。server.ts から Express インスタンスの作成部分を app.ts ファイルに移動します。

app.ts

import 'zone-js/node';

import * as express from 'express';
import { join } from 'path';

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

export const app = express();
const distFolder = join(process.cwd(), 'dist/my-ssr-app/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModule,
}));

app.set('view engine', 'html');
app.set('views', distFolder);

// Example Express API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
app.get('*.*', express.static(distFolder, {
  maxAge: '1y'
}));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});

server.ts

import { app } from './app';

function run(): void {
  const port = process.env['PORT'] || 4000;

  // Start up the Node server
  const server = app;
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// Call the run function to start up the server
run();

handler.ts を作成する

Lambda のエントリポイントとなる handler.ts ファイルを作成します。ここでは、@h4ad/serverless-adapter ライブラリを使って、Lambda イベントを Express アプリケーションに渡します。

import { app } from './app';
import { Server } from 'http';
import { createServer, proxy } from '@h4ad/serverless-adapter';

let server: Server;

export const handler = async (event: any, context: any) => {
  if (!server) {
    server = createServer(app);
  }
  return proxy(server, event, context);
};

handler.ts は、Lambda 関数が呼び出されたときに実行されるコードです。@h4ad/serverless-adapter ライブラリを使って、Lambda イベントを Express アプリケーションに渡しています。

angular.json を編集する

angular.json ファイルを編集し、SSR ビルド時のエントリポイントを handler.ts に変更します。

"projects": {
  "my-ssr-app": {
    // ... (その他設定)
    "architect": {
      "server": {
        // ... (その他設定)
        "options": {
          // ... (その他設定)
          "main": "server.ts",
          "outputPath": "dist/my-ssr-app/server",
        },
        "configurations": {
          "production": {
            // ... (その他設定)
            "main": "handler.ts", // エントリポイントを変更
            "outputPath": "dist/my-ssr-app/serverless",
          }
        }
      },
      // ... (その他設定)
    }
  }
}

angular.jsonprojects.<project-name>.architect.server.configurations.production.main プロパティを handler.ts に変更することで、SSR ビルド時に handler.ts がエントリポイントとして使用されます。

tsconfig.json を継承した tsconfig.sls.json を作成する

Serverless Framework との互換性のために、tsconfig.json を継承した tsconfig.sls.json ファイルを作成し、compilerOptions.modulecommonjs に設定します。また、tsconfig.app.jsonapp.tshandler.ts を追加します。

tsconfig.sls.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "module": "commonjs", // commonjs に変更
    "types": ["node"]
  },
  "include": ["handler.ts", "app.ts"]
}

tsconfig.app.json

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts",
    "app.ts", // 追加
    "handler.ts" // 追加
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}

package.json にコマンドを追加する

package.json ファイルに、Serverless Framework を使ったビルドとデプロイのコマンドを追加します。

{
  // ... (その他設定)
  "scripts": {
    // ... (その他スクリプト)
    "serverless": "TS_NODE_PROJECT=tsconfig.sls.json serverless offline start",
    "deploy": "ng build --configuration production && serverless deploy"
  }
}

serverless コマンドは、ローカル環境で Lambda 関数を実行するためのコマンドです。deploy コマンドは、アプリケーションをビルドし、Lambda にデプロイするためのコマンドです。TS_NODE_PROJECT 環境変数で、serverless コマンドで使用する tsconfig.json ファイルを指定しています。

デプロイしてみる

npm run deploy コマンドを実行して、Lambda にアプリケーションをデプロイします。

npm run deploy

デプロイが完了すると、ターミナルに Lambda Function URL が表示されます。ブラウザでその URL にアクセスすると、SSR された Angular アプリケーションが表示されます。

本番運用で考慮したいこと

本番環境で SSR アプリケーションを運用する際には、以下の点を考慮する必要があります。

  • CDN を活用する: 静的ファイルを CDN でキャッシュすることで、Lambda 関数へのリクエスト数を減らし、コストを削減することができます。
  • HTML のキャッシュを無効にする: HTML レスポンスヘッダーに Cache-Control: no-cache を設定することで、CDN のキャッシュが無効になり、常に最新の HTML が配信されるようになります。

Angular Material は?

Angular v17 から、Angular Material の SSR が正式にサポートされました。ただし、CSR (Client-Side Rendering) 専用の機能は SSR 環境では動作しない可能性があるため、注意が必要です。Angular Material の SSR に関しては、公式ドキュメントでより詳細な情報が提供されることを期待しています。

ISR もできるらしい

RxAngular が ISR (Incremental Static Regeneration) を実装するためのライブラリを公開しています。機会があれば、このライブラリを使って ISR を試してみたいと思います。ただし、サンプルコードは少し古くなっているため、調整が必要になるかもしれません。また、Lambda 環境で ISR を実装する場合は、DynamoDB などを使って共有キャッシュ層を実装する必要があるでしょう。

SSR 用のフレームワークの選定

この記事では、SSR 用のフレームワークとして Express を使用しましたが、Fastify などの他のフレームワークを使用することもできます。Fastify は、Express よりも高速で軽量なフレームワークであり、Lambda 環境での使用に適しています。パフォーマンスとコストを重視する場合は、Fastify の使用を検討する価値があります。

まとめ

Angular v17 で SSR 機能が Angular CLI に統合されたことで、SSR アプリケーションの開発がより簡単になりました。SEO や初期表示速度が重要なアプリケーションでは、SSR を積極的に採用する価値があります。

React 系フレームワークに押され気味だった Angular ですが、SSR 機能の強化によって、再び競争力を高めることができるでしょう。Angular SSR を使って、本番環境で動作するアプリケーションを開発してみてはいかがでしょうか。

参考文献

Angular Official Documentation: Universal

よくある質問 (Q&A)

  • Q: SSRを使用する主な利点は何ですか?
    A: 主な利点は、初回のページロードが速くなること、SEO効果が向上すること、ユーザー体験が改善されることです。
  • Q: プロジェクトにSSRを追加するのは難しいですか?
    A: ある程度の知識が必要ですが、Angular Universalを使用することで比較的簡単に実装できます。
  • Q: SSRはどのようにSEOに影響しますか?
    A: SSRによってサーバー側でHTMLが生成されるため、クローラーがコンテンツを容易にインデックスできます。