Serverless is traditionally used for very small functions that do one thing and do it well. But what if that one small thing has one large dependency? For example, what if your function depends on Headless Chrome? Can you really fit an entire web browser in a serverless function?

View Demo

Our goal is to somehow ship a web browser and lambda function that can take a screenshot of any arbitrary website in under 50 MB. This might prove difficult because Google Chrome on MacOS is around 400 MB. Similarly, puppeteer is around 300 MB because it downloads Chromium during npm install before exposing the API. No surprise there.
Fortunately, the puppeteer documentation mentions a smaller puppeteer-core package, only 2 MB, that can connect to our own Chromium instance of choice. We can use that to take a screenshot, but we still need a super small version of Chromium that will run in a serverless environment.
Enter chrome-aws-lambda, a brotli-compressed version of Chromium designed for serverless environments, weighing in at only 35 MB! With a few lines of code, we can wire up these two packages to take a screenshot of a webpage!
Let's create a screenshot.js file with the following code.
const chrome = require('chrome-aws-lambda');
const puppeteer = require('puppeteer-core');

async function getScreenshot(url, type) {
    const browser = await puppeteer.launch({
        args: chrome.args,
        executablePath: await chrome.executablePath,
        headless: chrome.headless,
    });

    const page = await browser.newPage();
    await page.goto(url);
    const file = await page.screenshot({ type });
    await browser.close();
    return file;
}
Looks great! Of course, we want to deploy this code to Now 2.0. So we'll need to export a single lambda function that will read the HTTP Request and then write the HTTP Response. Think of this lambda as the same function that you pass to http.createServer(lambda) in any Node.js app.
    const { type = 'png' } = query; // png or jpeg
    let url = pathname.slice(1);
    if (!url.startsWith('http')) {
        url = 'https://' + url; // add protocol if missing
    }
    const file = await getScreenshot(url, type);
    res.statusCode = 200;
    res.setHeader('Content-Type', `image/${type}`);
    res.end(file);
};
That wasn't too difficult. Next, let's add the following now.json file so that our deployment knows to use a node builder.
{
    "version": 2,
    "builds": [
        { "src": "screenshot.js", "use": "@now/node" }
    ],
    "routes": [
        { "src": "/(.*)", "dest": "/screenshot.js" }
    ]
}
Since our file containing the lambda function is named screenshot.js, we use the builds key to map a file (or glob pattern) to a builder, in this case Node.
Then we use the routes key to map a route to a file. In this case, we use a regex to map all routes to our lambda function.
Let's deploy our code by running now from the command line and see what happens.
Error: The lambda function size (34.75mb) exceeds the configured limit (5mb). You may increase this by supplying `maxLambdaSize` to the build `config`
We are seeing this error because the node builder has a default limit of 5 MB. This limit is meant to prevent unintended consequences, such as slow response times or increased cloud bills.
It's clear that we need to increase the lambda size so let's bump the limit to 40 MB which should give us a good upperbound. If we exceed this limit, the deployment will fail and we will know something is wrong.
{
    "version": 2,
    "builds": [
-        { "src": "screenshot.js", "use": "@now/node" }
+        { "src": "screenshot.js", "use": "@now/node", "config": { "maxLambdaSize": "40mb" } }
    ],
    "routes": [
        { "src": "/(.*)", "dest": "/screenshot.js" }
    ]
}
Ok let's deploy again by running now from the command line. Success! 🎉
Visiting our deployment with a route such as /google.com will return a screenshot of Google's homepage. We did it!
Try out the final result at screenshot-v2.now.sh and dig into the source code at zeit/now-examples on GitHub.