Main

[db]

Packaging JS Apps with QuickJS

Posted by Dustin on in .

QuickJS is a tiny JavaScript engine written in C. Its author, Fabrice Bellard, created FFMPEG and QEMU. QuickJS can run outside of traditional browser environments. This includes packaging JavaScript applications for distribution.

As of writing, QuickJS supports ECMAScript 2023, so you can write modern JavaScript. It is also fast and well-tested.

One of the engine’s features is compiling JavaScript code into bytecode. Bytecode is a compact set of instructions that an interpreter can execute. It makes execution more efficient, which is perfect where performance is important. Additionally, the engine supports compilation of JavaScript into dependency-free standalone executables. We will use this feature to package JavaScript apps.

QuickJS’s small footprint is also suitable for embedded systems and resource-constrained environments.

Recently, Amazon announced its Low Latency Runtime (LLRT), built on QuickJS. According to Amazon, LLRT starts 10x faster and is 2x less expensive than other JS runtimes on AWS Lambda. That’s a pretty impressive use case.

Installation

Installing QuickJS in your development environment is straightforward. Here’s how to get started. Note that on Windows, you can use WSL with Linux.

Before jumping in, make sure you have Make and a C compiler like GCC or Clang.

The first step is to get a copy of the source code. You can download the source from the QuickJS website or by cloning it from the GitHub repository. We’ll download it from the website and untar the file:

wget https://bellard.org/quickjs/quickjs-2024-01-13.tar.xz
tar -xJf quickjs-2024-01-13.tar.xz

This command creates the directory quickjs-2024-01-13 with all the necessary source files. I’ll refer to this directory as quickjs from now on.

Compatibility

QuickJS does not rely on V8, WebKit, or Gecko. It is not compatible with NodeJS or Deno APIs. It does have access to the OS through its own APIs. To make sure your code is compatible with QuickJS, ensure the following:

  • Your code uses ECMAScript 2023 or lower
  • You are not using browser-specific APIs
  • You are not using Node.js or Deno-specific APIs

QuickJS focuses on the core JavaScript language, so your code should be platform-agnostic.

Organization

For this simple Hello World app we’re putting our files into the quickjs directory. In a business context, you should keep your code separate from QuickJS. Otherwise, you can structure your project as you would any other JavaScript project. You can even use modules.

Building QuickJS

Once you have the source code, the next step is to compile it. This will convert the code into an executable you can run on your computer. Navigate to the quickjs directory with cd quickjs.

Finally, compile the source with the make command. QuickJS’s Makefile will detect your OS and choose the appropriate compiler and flags. Run:

make

On macOS, Make will use Clang as the default compiler, whereas on Linux, GCC is more common. Compilation will take a few moments. Once completed, several new files will be added to the quickjs directory. qjs is the command-line tool for executing JavaScript files. qjsc is a tool for compiling JavaScript into bytecode.

Testing QuickJS

You can run a simple JavaScript file to verify your installation. Create a file named hello.js with the following code:

console.log("Hello, QuickJS!");

Save the file into the quickjs directory and then execute it using the qjs binary:

./qjs hello.js

If QuickJS installed, you will see the message “Hello, QuickJS!” Now you have QuickJS set up and ready, it’s time to package an application.

Packaging JavaScript

To create a standalone executable, use the qjsc executable in the quickjs directory. You can execute it with these commands:

qjsc -o hello hello.js
./hello

If it works, you will see “Hello, QuickJS!” on your terminal. This is the file that we will distribute.

There are many flags that you can pass to qjsc. Try experimenting with each. For example, output bytecode instead of an executable. Or disable regular expressions to decrease binary size.

Package Size

The executable for this “Hello, World!” example is 4.6 MB. It may seem large for such a simple program. However, consider the alternatives. Using deno compile I get an executable that is 76 MB. Compiling the hello.js file using Node 21 produces a 98 MB file. So, In perspective 4.6 MB seems pretty good.

Distribution

You don’t need anything special to distribute a QuickJS-packaged application. Using the standalone executables, you have a range of distribution options.

QuickJS is portable. That means that it can run in many environments. When preparing for distribution, consider the target platforms. If you want your application to work on Windows, macOS, and Linux, you must build the app on each system.

Closing Thoughts

Whether you’re developing IoT devices or building server-side tools QuickJS is a stellar option. It’s easy to use, fast, and produces relatively tiny executables.

Try experimenting with it, push its boundaries, and see how it can be used in your projects. What has your experience with QuickJS been? I’d love to hear your stories, successes, and lessons learned.