Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

@jmmitchell jmmitchell commented Mar 28, 2017

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

This comment has been minimized.

Copy link
Owner Author

@alexcasalboni alexcasalboni commented Mar 28, 2017

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

This comment has been minimized.

Copy link

@jmmitchell jmmitchell commented Mar 28, 2017

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

This comment has been minimized.

Copy link
Owner Author

@alexcasalboni alexcasalboni commented Mar 30, 2017

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