How this small change saved us over €2,400 yearly
I develop and maintain an ecommerce solution for restaurants and snackbars. A big part of our solution is streamlining the production process for takeout and delivery orders.
At the heart of the production station, we install a tablet and a thermal printer, which will print the incoming orders and really help the merchant to keep their operations running smooth.
We support a wide variety of printers, which all come with their own specs and SDK’s. Every printer manufacturer implements the features a bit different than the other. This meant that the receipt printing had to be reimplemented for every printer manufacturer - not ideal.
On top of that, some printers models of the same manufacturer shipped with a different firmware version, that lead to weird bugs and issues in the field.
Think of it like dealing with cross-browser compatibility back in the earlier days of web development, but worse.
Most printers implemented some form or subset of the ESC/POS protocol, which gets its name because every command starts with an escape character. It’s invented by Epson and broadly used in thermal printers today. Other companies, like Star for example, invented their own language, called StarPRNT, which is heavily inspired by ESC/POS, but just a bit different.
In an effort to reduce development time, we built a system that created screenshots of an HTML page, which we could then send to the printer.
This removed the hassle of implementing low-level protocols for every printer (like changing the font size or weight), increased consistency between printers, and massively improved the flexibility of the receipts.
We created a small interface that allowed our merchants to design their own receipt, which ultimately rendered HTML and CSS.
wkhtmltoimage
Our first version wrapped the wkhtmltoimage binary and lived in our monolithic Laravel application. When a request came in, wkhtmltoimage generates a ticket for the specified order synchronously. The process of generating a snapshot usually takes around 1s, and during this time an HTTP connection is held open.
At peak times, we often ran into the situation that 40+ wkhtmltoimage instances were generating snapshots at the same time — eating up resources of our machine and causing delays our customers started noticing.
As the amount of orders placed grew, this solution was not sustainable anymore.
Playwright
When faced with these scaling challenges, we changed course and migrated to a serverless solution using Playwright. Playwright had a significant boost in speed over wkhtmltoimage and we chose Playwright over Puppeteer because of the easy integration on AWS Lambda.
Whenever a receipt was requested, a Lambda would spin up a headless version of Google Chrome, take a screenshot of the HTML page, and store the resulting image on an S3 bucket and return an URL that pointed to this screenshot.
We wanted to make this transition as transparent as possible for the clients using it, so we kept the original HTTP endpoint and simply introduced a 301 redirect to our new AWS API Gateway.
After making this change, we could stop worrying about scaling issues on our online system. The added benefit of the serverless function was that the rendering time was reduced by around 20%.
Playwright requires a decent amount of memory on our Lambda and at the peak we were doing around 30.000 invocations daily, which resulted in a monthly bill of around €200.
Now, when developing our offline-first point-of-sales app, we wanted to remove the dependency on an internet connection for printing receipts, so we had to make the choice between either implementing the printer protocol directly again, or figure out a way to generate these images offline.
Our point-of-sales hardware was pretty limiting, so running Playwright locally was simply not an option.
html-to-image
Luckily there was a beautiful library called html2image, that is able to locally create snapshots of a DOM node using a canvas element.
The way this works is by cloning your specified DOM node inside an SVG file using <foreignObject />
. ForeignObject is a feature of SVG that allows having HTML content inside of SVG and the browser is able to render it correctly. This SVG is then copied onto a canvas node, which can ultimately be copied as an image.
No internet required
We re-implemented our receipt builder module client-side using a templating library called Nunjucks, which made it easy to render HTML.
After the HTML string was rendered, we injected it a dedicated iframe and used html-to-image to grab a snapshot of this iframe. We could then feed the resulting image to the printer over USB or local network.
The results were very good: by cutting out the network requests to the serverless function and the overhead of Playwright, we saved around 40% of time, and thus reducing the rendering time to less than 0.5s.
The feedback we were hearing from our merchants was great, so we decided to roll out this offline rendering approach to all our other systems until we could shut down our serverless function and start saving money.
The simple fact of generating these receipt images locally, saved us over €2,400 yearly, which greatly outweighs the development effort required to implement this.
And there you have it – our journey from battling with printer compatibility issues and scaling challenges, to solving this with Playwright on AWS Lambda, and finally landing on a local solution with html-to-image.
A journey that not only cut our costs but significantly improved performance and stability.
This evolution not only highlights the importance of adapting to new technology but also showcases how sometimes taking a step back and looking for different solutions can lead to substantial cost savings.