Skip to content

Instantly share code, notes, and snippets.

@alexcasalboni
Last active September 8, 2023 08:44
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save alexcasalboni/046471a7dcfbb6c468f843361c474aff to your computer and use it in GitHub Desktop.
Save alexcasalboni/046471a7dcfbb6c468f843361c474aff to your computer and use it in GitHub Desktop.
Serve dynamically generated, minimized and compressed HTML pages with AWS Lambda@Edge.

AWS Lambda@Edge Experiment

Requirements

  • AWS Lambda@Edge (enabled Preview)
  • One Amazon CloudFront Distribution (origin doesn't matter)
  • IAM role (basic execution is enough)
  • npm to install Node.js dependencies

Lambda Function Details

The Lambda@Edge will be invoked whenever a new "Viewer Request" event is triggered by CloudFront.

The Lambda Function will behave as follows:

  1. If the requested resource is NOT available locally (i.e. not an HTML file), the request can proceed to the origin
  2. If the local template exists, it will be read and rendered using Plates with a few dynamic variables (i.e. "title" and "today)
  3. The resulting HTML is then minified and eventually compressed, based on the request HTTP headers (response headers are correctly set as well)
  4. The final HTTP body is directly returned to the client without hitting the CloudFront origin

How to create the Deployment Package

cd this-gist

npm install

zip -r ../edge-deployment-package.zip ./*

Known Limitations

  • The deployment package cannot exceed 1MB, and a manual hack was required to include the 'html-minifier' library (i.e. reducing its size from 3MB to 500KB)
'use strict';
const fs = require('fs');
const zlib = require('zlib');
const Plates = require('plates');
const minify = require('html-minifier').minify;
const supportedCompression = ['gzip', 'deflate'];
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
// read local file
fs.readFile('/hook/unzipped' + request.uri.split('?')[0], function (err, data) {
if (err) {
callback(null, request); // bypass Lambda@Edge
} else {
renderHTML(data.toString(), request, callback);
}
});
};
function renderHTML(html, request, callback) {
const data = {
"title": "This is a Lambda@Edge test!",
"today": new Date().toString(),
};
// generate body and bind variables
const body = minifyHTML(Plates.bind(html, data));
// detect compression from request headers
const compression = detectCompression(request);
const response = {
status: '200',
statusDescription: 'HTTP OK',
httpVersion: request.httpVersion,
body: compressBody(body, compression),
headers: {
'Vary': ['*'],
'Content-Type': ['text/html; charset=UTF-8'],
'Last-Modified': ['2017-02-09'],
'Content-Encoding': [compression || 'UTF-8']
},
};
callback(null, response); // return custom response
}
function minifyHTML(html) {
return minify(html, {
collapseWhitespace: true,
removeComments: true,
});
}
function detectCompression(request) {
const accept = request.headers['Accept-Encoding'] || [];
for(var i = 0; i < accept.length; i++) {
if (supportedCompression.indexOf(accept[i]) !== -1) {
return accept[i]; // return the first match
}
}
return null;
}
function compressBody(body, compression) {
if (compression === 'gzip') {
return zlib.gzipSync(body).toString('utf8');
} else if (compression === 'deflate') {
return zlib.deflateSync(body).toString('utf8');
} else {
return body; // no compression
}
}
{
"name": "gulp-htmlmin",
"description": "AWS Lambda@Edge Experiment",
"version": "0.0.1",
"author": {
"name": "Alex Casalboni",
"url": "https://github.com/alexcasalboni/"
},
"dependencies": {
"plates": "0.4.11",
"html-minifier": "3.3.1"
}
}
<html>
<head>
<title>Test</title>
</head>
<body>
<h1 id="title"></h1>
<p id="today"></p>
</body>
</html>
@jmmitchell
Copy link

Have you checked to see if the built-in GZIP support (https://aws.amazon.com/blogs/aws/new-gzip-compression-support-for-amazon-cloudfront/) is still functional when running a Lambda@Edge function? If so, you might not need the GZIP library.

@alexcasalboni
Copy link
Author

Hi @jmmitchell,

That's a good point, and I have to check whether the built-in gzip happens before or after hitting/missing the cache. My Lambda@Edge Function is running on "Viewer Request", which means before trying to hit the cache (or the origin) since I wanted to achieve an "originless" execution.

I'm afraid that CloudFront will add to its cache and gzip only objects that come from the origin or from the "Origin Request/Response" Lambda@Edge Functions, in which case I can't count on the built-in gzip support.

I'll keep you posted :)

@jmmitchell
Copy link

It would be key to know as this will certainly be helpful information for many others to know as well. Thanks for checking. I look forward to hearing back on what you find.

@alexcasalboni
Copy link
Author

It looks like you have to compress the response yourself, if you are generating a dynamic reponse on 'Viewer Request'.

You can find out more at the HTTP Response with Compressed Static Content section here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment