I’ve heard good things about Deno, so I thought I would try it out. The project I decided to try it with requires streaming a whole lot of SVG to the browser. One of the first things I did was to try to get Deno to compress the stream (Server-Sent Events, SSE). It matters a lot with the amount of SVG I’m streaming! But Deno does not support compressing SSE streams for some reason. This is kinda annoying as it should be fairly straightforward to compress when an event is flushed, but I don’t have the full picture so who knows. I had a similar experience with NestJS, so this isn’t unique to Deno.

Caddy

No matter, the web server in front of Deno can do the compression. Caddy has support for Gzip and Zstd. Zstd is great, but support is still not 100%, in particular Safari on older MacOS and iOS doesn’t support it. I don’t want to fall back to Gzip, since it’s so much worse. There’s a third option: Brotli. It’s about as good as Zstd, and “all browsers” support it. Brotli isn’t part of Caddy tho. It’s not included in Nginx either. Traefik does have it, but it lacks other features I need here.

As usual I’m using Devenv for this project. Devenv is built on Nix. Turns out it’s pretty easy to create a new Nix package (a derivation in Nix-speak) that builds a custom Caddy with Brotli support. If you don’t use Nix or devenv, you can jump to the Caddyfile part because there’s still relevant information there.

nix/caddy-brotli.nix:

# Usage:  pkgs.callPackage ./caddy-brotli.nix { }
# See Caddyfile usage here: https://github.com/ueffel/caddy-brotli
{
  caddy,
}:

caddy.withPlugins {
  plugins = [
    "github.com/ueffel/caddy-brotli@v1.6.0"
  ];

  # When new versions of Caddy or the plugin are released the hash needs to be updated:
  hash = "sha256-OVbqh6dCq9iZ8Siy78bNrRJyHMxSiLlLKIdnwus4qk8=";
}

And usage in devenv.nix:

The derivation could be used from a flake or regular Nix config as well.

{
  pkgs,
  lib,
  config,
  inputs,
  ...
}:

let
  caddyWithBrotli = pkgs.callPackage ./nix/caddy-brotli.nix { };
in
{
  services.caddy = {
    package = caddyWithBrotli;
    enable = true;
    config = ''
      :8080 {
        encode {
          zstd
          br 4
        }
        reverse_proxy 127.0.0.1:8000
      }
    '';
  };
  # ... the rest of the file.
}

The Caddyfile part:

:8080 {
  encode {
    zstd
    br 4
  }
  reverse_proxy 127.0.0.1:8000
}

The docs for the br part: https://github.com/ueffel/caddy-brotli

It’s important to put zstd before the br line. It gives priority to zstd for those browsers that support both. You want that because the Brotli implementation we embedded in Caddy isn’t ideal. At higher quality settings (above 4), it’s slow, and there’s no window size configuration exposed, which would be an important knob for some types of streams. There’s another Brotli plugin for Caddy that I didn’t use because it’s trickier to get compiled. The derivation would be more complex and it’s not worth it when most browsers can use zstd. I just want to make it good enough for the older Safaris.

The other plugin: https://github.com/dunglas/caddy-cbrotli

Deploy the custom Caddy

Devenv is able to build Docker images as well, so I’ll probably make my own Caddy image based on this same derivation when the time comes to deploy it. Thanks to Devenv and its lockfile, the image will use the exact same package versions as my development environment.