AWS React SSR architectures

Oleksii Kosynskyi

by

Hello

Oleksii Kosynskyi

Senior Software Engineer

at ChainSafe (web3.js)

 

 

 

 

 

 

 

avkos

 

avkosinski

 

avkosinski

SSR

What is it?

Why SSR?

- SEO optimization

- Fast page load

Why cloud?

- SSL

- Cache settings

- Distributed execution (regions)

- Scaling

Lambda implementation

import {renderToString} from 'react-dom/server'


export const handler = async (event) => {
       
        const props = await getSomeDataFromDB(event)
        
        const appString = renderToString(<App {...props}/>)
        
        return {
            statusCode: 200,
            headers: {"Content-Type": "text/html"},
            body: insertAppToIndexHTML(appString)
        };
}

ApiGateway

SSR

LambdaEdge

SSR

LambdaEdge

CloudFront

LambdaEdge

Lambda

Max execution time

5s

15 min

Max response size

1Mb

-

Limitation

LambdaEdge

Lambda

1 mln requests

Free Tier

1 mln requests and 400 000 Gb-seconds executions per month

0,60 USD

-

Gb-seconds

0,00005001 USD

0,20 USD

0,0000166667 USD

Price

Lambda EDGE

Lambda API GATEWAY

Compress

CloudFront

Compress

Lambda Edge

import zlib from 'zlib'
...
const gzip = (html) => new Promise((resolve, reject) => {
    const input = Buffer.from(html);
    zlib.deflate(input, (err, res) => {
        if (err) {
            return reject(err)
        }
        resolve(res.toString('base64'))
    });
})

...

await gzip(html)
...

Lambda Edge Response

{
   status: "200",
   statusDescription: "OK",
   bodyEncoding: 'base64',
   headers: {
      "cache-control": [
          {
               key: "Cache-Control",
               value: "max-age=100",
          },
      ],
      "content-type": [
          {
               key: "Content-Type",
               value: "text/html",
          },
      ],
      "content-encoding": [
          {
               key: 'Content-Encoding',
               value: 'deflate'
          }
      ]
  },
  body,
}

Set up resources

Need to upload files directly from S3 without using Lambda

{
    test: /\.(svg|jpeg|jpg|gif|png)$/,
    use: [{
      loader: 'file-loader',
      options: {
                
                 publicPath: url => url, 
                 
                 emitFile: false, 
                 
                 name(resourcePath) {
                      const filename = path.basename(resourcePath); 
                    
                      return clientManifest.files[`static/media/${filename}`]; 
                 },
              },
    }],
},
...
plugins: [
   new webpack.DefinePlugin({
      PUBLIC_URL: JSON.stringify(process.env.PUBLIC_URL),
   })
]

IMAGE_INLINE_SIZE_LIMIT=0

Set up deploy

AWS CDK

SSR API GATEWAY STACK

export class AppSsrApiStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
    
        super(scope, id, props);
        
        const staticConstruct = new StaticConstruct(this, 'StaticStack', {
            buildPath: '../app/build-prod/',
            bucketName: 'static-ssr-api'
        })
        
        new SsrApiConstruct(this, 'SsrApiStack', {
            region: this.region,
            staticS3Bucket: staticConstruct.staticS3Bucket,
            originAccessIdentity: staticConstruct.originAccessIdentity
        })
        
    }
}
export class StaticConstruct extends Construct {
    public staticS3Bucket: s3.Bucket
    public originAccessIdentity: cloudfront.OriginAccessIdentity

    constructor(scope: Construct, id: string, props: TProps) {
    
        super(scope, id);

        this.staticS3Bucket = new s3.Bucket(...);

        this.originAccessIdentity = new cloudfront.OriginAccessIdentity(...);
        
        this.staticS3Bucket.grantRead(this.originAccessIdentity);

        new s3deploy.BucketDeployment(...);
        
    }
}
export class SsrApiConstruct extends Construct {
    constructor(scope: Construct, id: string, props: TProps) {
        
        super(scope, id);
        
        const ssrLambdaApi = new lambda.Function(...)
        
        const ssrApi = new apigw.LambdaRestApi(...)
        
        const ssrApiDistribution = new cloudfront.CloudFrontWebDistribution(...);
        
    }
}
            {
                originConfigs: [{
                        s3OriginSource: {...S3Props},
                        behaviors: [
                            {
                                pathPattern: '/static/*.*',
                            },
                            {
                                pathPattern: '/static/js/*.*',
                            },
                            {
                                pathPattern: '/static/css/*.*',
                            },
                            {
                                pathPattern: '/favicon.ico',
                            },
                            {
                                pathPattern: '/manifest.json',
                            }
                        ]
                    }, {
                        customOriginSource: { ...apiGWProps },
                        behaviors: [
                            {
                                isDefaultBehavior: true,
                            }
                        ]
                    }
                ]
            }
  

SSR LAMBDA-EDGE STACK

export class AppSsrEdgeStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
    
        super(scope, id, props);
        
        const staticConstruct = new StaticConstruct(this, 'StaticEdgeStack', {
            buildPath: '../app/build-prod/',
            bucketName: 'static-ssr-edge'
        })
        
        new SsrEdgeConstruct(this, 'SsrEdgeStack', {
            staticS3Bucket: staticConstruct.staticS3Bucket,
            originAccessIdentity: staticConstruct.originAccessIdentity
        })
        
    }
}
export class StaticConstruct extends Construct {
    public staticS3Bucket: s3.Bucket
    public originAccessIdentity: cloudfront.OriginAccessIdentity

    constructor(scope: Construct, id: string, props: TProps) {
    
        super(scope, id);

        this.staticS3Bucket = new s3.Bucket(...);

        this.originAccessIdentity = new cloudfront.OriginAccessIdentity(...);
        
        this.staticS3Bucket.grantRead(this.originAccessIdentity);

        new s3deploy.BucketDeployment(...);
        
    }
}
export class SsrEdgeConstruct extends Construct {
    constructor(scope: Construct, id: string, props: TProps) {
    
        super(scope, id);

        const ssrLambdaEdge = new lambda.Function(...);

        const ssrEdgeFunctionVersion = new lambda.Version(...);

        const ssrEdgeDistribution = new cloudfront.CloudFrontWebDistribution(...);
        
    }
}
           originConfigs: [{
                   s3OriginSource: { ...S3Props },
                   behaviors: [
                       {
                           pathPattern: '/static/*.*',
                       },
                       {
                           pathPattern: '/static/js/*.*',
                       },
                       {
                           pathPattern: '/static/css/*.*',
                       },
                       {
                           pathPattern: '/favicon.ico',
                       },
                       {
                           pathPattern: '/manifest.json',
                       }
                   ]
               }, {
                   s3OriginSource: { ...S3Props },
                   behaviors: [{
                           isDefaultBehavior: true,
                           lambdaFunctionAssociations: [{
                                   eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
                                   lambdaFunction: ssrEdgeFunctionVersion
                           }]
                       }]
               }]
  

Deploy to cloud

Demo - Time

SSR-APIGW

SSR-EDGE

SSR CHECK

Test different regions

SSR LAMBDA-EDGE

SSR LAMBDA-EDGE

(Second test)

SSR LAMBDA API GW

Set up local start

AWS CDK

yarn cdk synth AppSsrApiStack 

sam local start-api -t ./cdk.out/AppSsrApiStack.template.json

Set up local start

Serverless Framework

service: lambda-ssr
frameworkVersion: '2 || 3'
app: lambda-ssr

custom:
  serverless-offline:
    httpPort: 3005
    includeModules: true # add excluded modules to the bundle
    noPrependStageInUrl: true #remove stage /dev from url
  webpack:
    webpackConfig: 'webpack.config.js' # Name of webpack configuration file
    includeModules:
    packager: 'yarn' # Packager that will be used to package your external modules
    excludeFiles: /**/*.test.js # Provide a glob for files to ignore

provider:
  name: aws
  lambdaHashingVersion: '20201221'
  runtime: nodejs14.x

plugins:
  - serverless-webpack
  - serverless-offline
functions:
  ssr:
    name: lambda-ssr
    handler: ssr.apiHandler
    events:
      - http:
          method: GET
          path: /
          cors: true
      - http:
          method: ANY
          path: /{proxy+}
          cors: true

Static files

import {apiHandler as apiHandlerSsr} from './ssr'
import express from 'express'
import serverlessExpress from '@vendia/serverless-express'

export const apiHandler = apiHandlerSsr

const app = express();
app.use(express.static(__dirname + '/../../build-local/static'));
app.use(express.static(__dirname + '/../../build-local'));

export const staticHandler = serverlessExpress({app})
  static:
    name: lambda-static
    handler: ssr.staticHandler
    events:
      - http:
          method: GET
          path: /static/{proxy+}
          cors: true
      - http:
          method: GET
          path: /favicon.ico
          cors: true
      - http:
          method: GET
          path: /manifest.json
          cors: true
      - http:
          method: GET
          path: /logo192.png
          cors: true
watch 'yarn build' ./src
sls offline

2

1

Local start

Local start

GitHub

Helpful links

AWS CDK

https://cdkworkshop.com/20-typescript.html

 

REACT APP ENVs

https://create-react-app.dev/docs/advanced-configuration/

 

SERVERLESS - NEXTJS

https://github.com/serverless-nextjs/serverless-next.js

?

React Serverless SSR

By Alexey Kosinski

React Serverless SSR

  • 308