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
- 407