Amazon Web Services ブログ

Next.js 13 の SSR Streaming を AWS Lambda Response Streaming で実装する方法

AWS Lambda のレスポンスペイロードストリーミングを使用することで、サーバーサイドでレスポンスデータが利用可能になった時点で呼び出し元にデータを段階的に送信することができます。これにより、Web アプリケーションやモバイルアプリケーションのパフォーマンスを改善し、ユーザ体験を向上させることができます。AWS Lambda のレスポンスストリーミングに関して詳しくは Introducing AWS Lambda response streaming の記事を参照ください。

本記事では、レスポンスストリーミングの実践的な活用例として、Next.js 13 において提供されている SSR Streaming を AWS Lambda において動作させ、 HTML 要素を段階的/ストリーミング的にブラウザに表示する方法を紹介します。また、Next.js の静的コンテンツを Amazon CloudFront の Edge Location においてキャッシュさせる方法についても説明します。

※ 本記事では Next.js のバージョン v13.4.2 を使用しています。

Next.js 13 の SSR Streaming と Selective Hydration

一般的に、React におけるサーバーサイドレンダリング (SSR) では、特定のページのすべてのデータをサーバー側で取得し、HTMLページとしてレンダリングした後、クライアントにコンテンツを返却します。クライアントでは HTML と CSS を使用して非インタラクティブな画面が表示されます。最後に Hydrate 処理が行われることで JavaScript のイベントを受け取れるインタラクティブな状態となります。

Hydrate とは?
React によってサーバーサイドレンダリングされたページは単純にクライアントに返却するだけでは JavaScript のイベントを受け取れるインタラクティブな状態にはなりません。onClick プロパティなどで渡されたイベントリスナーを DOM に登録する必要があります。このイベントリスナーを DOM に登録する処理を Hydrate と呼びます。

このプロセスにおいて、大きく2つの課題があります。サーバーサイドレンダリングにおいては、サーバーサイドですべてのデータを取得しなければ、HTML ファイルをクライアントに返却することができず、Time to First Byte (TTFB) が長くなってしまう傾向があります。また、全ての JavaScript がクライアントに返却されるまで Hydrate 処理が実行できない、あるいは Hydrate 処理が完全に終わるまでクライアントでユーザの入力を受け付けることができるインタラクティブな状態にならないという課題があります。

SSR Streaming や Selective Hydration を活用することで、これらの課題を解消することができます。SSR Streaming の方法をとることで、サーバーサイドで生成されたコンポーネントの単位でクライアントにデータを返却することができます。また、Selective Hydraiton により、ユーザの操作(ページスクロールやカーソルのアタッチなど)に応じて、選択的にコンポーネントをインタラクティブにすることができます。詳細は Next.js のドキュメント を参照ください。

この SSR Streaming 機能では、レスポンスヘッダーに HTTP/1.1 における Transfer-Encoding: chunked を付与した Chunked transfer encoding による HTTP Streaming の通信形式が採用されています。

AWS Lambda Web Adapter を使用して Next.js を Lambda で起動する

Next.js をサーバーとして起動する場合、特定のポート(3000番など)をリッスンして動作することになります。対して、AWS Lambda は特定のイベントに応じて起動するモデルを採用しています。AWS Lambda Web Adapter を使用することにより、Lambda ランタイムが受け取ったイベントを HTTP リクエストに変換し、ウェブサーバーのプロセスに転送できます。これにより、どのようなウェブサーバー、ウェブアプリケーションフレームワークでも Lambda で動作させることができます。より詳しくは こちらの builders.flash の記事もご参照ください。

AWS Lambda Web Adapter の使い方

Lambda Web Adapter を使用するには、Lambda を Docker Image としてデプロイする必要があります。使い方はシンプルで、以下のように Dockerfile に1行 Lambda Adapter をインストールするコマンドを記述するだけです。Lambda Adapter は Lambda Extension として実装されているため、/opt/extensions 配下にコピーする必要があります。

FROM public.ecr.aws/docker/library/node:16.13.2-stretch-slim

# 1行追加するだけ
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.0 /lambda-adapter /opt/extensions/lambda-adapter

ENV PORT=7000
WORKDIR "/var/task"
ADD src/package.json /var/task/package.json
ADD src/package-lock.json /var/task/package-lock.json
RUN npm install --omit=dev
ADD src/ /var/task
CMD ["node", "index.js"]

Lambda Response Streaming の機能は AWS Lambda Web Adapter v0.7.0 からサポートされています。また、Lambda Function の環境変数 AWS_LWA_INVOKE_MODE に response_stream を設定する必要があることに注意してください。

Next.js の Standalone モード

Next.js には本番環境で必要となるファイルのみをまとめて出力する Standalone モード があります。これによりビルド成果物のサイズを大幅に削減できます。なお、AWS Lambda ではデプロイされるパッケージのサイズに 250MB のクォータが設けられており、注意が必要です。

Next.js を Standalone モードでビルドすると、./next 配下に成果物が展開されます。以下は Next.js アプリケーションをビルドする Dockerfile のサンプルです。

FROM node:18-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build

FROM base AS runner
# Install Lambda Web Adapter
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.0 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=3000
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]

AWS Lambda Function URLs を使用して HTTPS エンドポイントとして公開する

AWS Lambda Function URLs の機能を使用することで、任意の Lambda 関数に HTTPS エンドポイントを追加できます。また InvokeMode を RESPONSE_STREAM として指定することで、AWS Lambda の Response Stream 機能を利用できます。より詳しくは こちらのブログ も参照ください。Response Stream を有効にした Function URL を AWS SAM で定義する方法は次のとおりです。

AWSTemplateFormatVersion: 2010-09-09

Description: >-
  Next.js 13 standalone mode
Transform:
  - AWS::Serverless-2016-10-31
  
Resources:
  NextjsLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
      MemorySize: 512
      Timeout: 30
      Description: Next.js 13 standalone mode function
      Environment:
        Variables:
          AWS_LWA_INVOKE_MODE: response_stream
    Metadata:
      DockerTag: nodejs18.x-v1
      DockerContext: ./frontend
      Dockerfile: Dockerfile
  StreamingUrl:
    Type: AWS::Lambda::Url
    Properties:
      TargetFunctionArn: !Ref NextjsLambdaFunction
      AuthType: NONE
      InvokeMode: RESPONSE_STREAM
  PermissionForURLInvoke:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref NextjsLambdaFunction
      FunctionUrlAuthType: "NONE"
      Action: lambda:InvokeFunctionUrl
      Principal: "*"

これで、Next.js を AWS Lambda にデプロイし、HTTPS のエンドポイントとして公開することができました。

lambda-response-stream-sample

なお、AWS Lambda Web Adapter を使用して Next.js をデプロイするサンプルコードはこちらのリポジトリを参照ください。

Amazon CloudFront を併用したリファレンスアーキテクチャ

Lambda Function URLs を使用してウェブサイトを公開するだけでなく、Amazon CloudFront のエッジロケーションにおいて静的コンテンツをキャッシュすることで、さらにパフォーマンス良く、ユーザ体験を向上させることができます。Next.js はページのレンダリング方法に応じてレスポンスに Cache-Control Header を付与します。以下はその例です。

レンダリング手法 Cache-Control Header
SSR (Server Side Rendering) private, no-cache, no-store, max-age=0, must-revalidate
SSG (Static Site Genration) s-maxage=31536000, stale-while-revalidate
ISR (Incremental Static Regeneration) s-maxage=60, stale-while-revalidate

これらの Cache-Control ヘッダーに対して、CloudFront のデフォルトキャッシュポリシー Caching Optimized を使用することで、多くの場合コンテンツを Edge ロケーションでキャッシュできます。ただし、このデフォルトのキャッシュポリシー Caching Optimized では、クエリ文字列がキャッシュキーに含まれないことに注意が必要です。Next.js の next/image が出力する画像ファイルは /_next/image* 配下にクエリ文字列を識別子として出力されます。そのため、/_next/image* 要素に関しては、CloudFront の Behavior およびカスタムキャッシュポリシーを個別に設定し、クエリ文字列もキャッシュキーの条件に含める必要があります。なお、そのほかのコンテンツに関してもキャッシュポリシーを見直したり、個別の CloudFront Behavior を設定することで最適化できる場合があります。

cloudfront-nextjs-architecture

オリジンへのリクエストを CloudFront からの通信のみに制限する

通常 CloudFront と S3 を併用した静的ウェブサイトホスティングの構成においては、OAC (Origin Access Control) によるアクセス制限を効かせることを推奨しています。CloudFront と Lambda Function URLs の構成においては、OAC によるアクセス制限をすることはできません。代わりに、 Next.js の middleware 機能を使用してオリジンからのアクセスを検証できます。CloudFront からのアクセスを特定するために任意のヘッダーを検証するか、CloudFront が利用する IP アドレスを判断し制御することができます。CloudFront が利⽤する IP アドレスは下記 URL から取得可能です。

CloudFront が利⽤する IP アドレス
https://docs.thinkwithwp.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/LocationsOfEdgeServers.html

おわりに

本記事では Lambda Response Streaming の実践的な活用例として、Next.js の SSR Streaming をご紹介しました。Response Streaming によってフロントエンドのユーザ体験を向上できる可能性があります。Next.js に限らずウェブアプリケーションやモバイルアプリケーションなどの様々な領域で AWS Lambda Response Streaming は活用できます。さらなる詳細については以下の参考文献も併せて参照ください。

参考文献

AWS Lambda レスポンスストリーミングの紹介
https://thinkwithwp.com/jp/blogs/news/introducing-aws-lambda-response-streaming/

Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化する
https://thinkwithwp.com/jp/builders-flash/202301/lambda-web-adapter/?awsf.filter-name=*all

AWS Lambda の上でいろんなWEB フレームワークを動かそう!
https://speakerdeck.com/_kensh/web-frameworks-on-lambda

形で考えるサーバーレス設計
https://thinkwithwp.com/jp/serverless/patterns/serverless-pattern/

サーバーレスの始め⽅
https://thinkwithwp.com/jp/serverless/patterns/redirect-serverless-steps

著者について

Awaji Daisuke

淡路 大輔 (Awaji Daisuke)

アマゾン ウェブ サービス ジャパン合同会社 ソリューションアーキテクト
現在は、公共部門のお客様向けの技術支援に従事しています。特にアプリケーションのユーザ体験(UI/UX)を専門とし、アプリケーションのモダン化の支援を行っています。仕事以外では、家族と過ごす時間、料理を楽しんでいます。