statigz
serves pre-compressed embedded files with http in Go 1.16 and later.
Since version 1.16 Go provides standard way to embed static assets. This API has advantages over previous solutions:
- assets are processed during build, so there is no need for manual generation step,
- embedded data does not need to be kept in residential memory (as opposed to previous solutions that kept data in regular byte slices).
A common case for embedding is to serve static assets of a web application. In order to save bandwidth and improve
latency, those assets are often served compressed. Compression concerns are out of embed
responsibilities, yet they
are quite important. Previous solutions (for example vfsgen
with httpgzip
) can optimize performance by storing compressed assets and
serving them directly to capable user agents. This library implements such functionality for embedded file systems.
Read more in a blog post.
package main
import (
"embed"
"log"
"net/http"
"github.com/vearutop/statigz"
"github.com/vearutop/statigz/brotli"
)
// Declare your embedded assets.
//go:embed static/*
var st embed.FS
func main() {
// Plug static assets handler to your server or router.
err := http.ListenAndServe(":80", statigz.FileServer(st, brotli.AddEncoding))
if err != nil {
log.Fatal(err)
}
}
Behavior is based on nginx gzip static module and
github.com/lpar/gzipped
.
Static assets have to be manually compressed with additional file extension, e.g. bundle.js
would
become bundle.js.gz
(compressed with gzip) or index.html
would become index.html.br
(compressed with brotli).
NOTE:
zopfli
provides better compression thangzip
while being backwards compatible with it.
Upon request server checks if there is a compressed file matching Accept-Encoding
and serves it directly.
If user agent does not support available compressed data, server uses an uncompressed file if it is available (
e.g. bundle.js
). If uncompressed file is not available, then server would decompress a compressed file into response.
Responses have ETag
headers (64-bit FNV-1 hash of file contents) to enable caching. Responses that are not dynamically
decompressed are served with http.ServeContent
for ranges support.
Support for brotli
is optional. Using brotli
adds about 260 KB to binary size, that's why it is moved to a separate
package.
NOTE: Although
brotli
has better compression thangzip
and already has wide support in browsers, it has limitations for non-https servers, see this and this.
Recommended way of embedding assets is to compress assets before the build, so that binary includes *.gz
or *.br
files. This can be inconvenient in some cases, there is EncodeOnInit
option to compress assets in runtime when
creating file server. Once compressed, assets will be served directly without additional dynamic compression.
Files with extensions ".gz", ".br", ".gif", ".jpg", ".png", ".webp" are excluded from runtime encoding by default.
NOTE: Compressing assets in runtime can degrade startup performance and increase memory usage to prepare and store compressed data.
It may be convenient to strip leading directory from an embedded file system, you can do that with statigz.FSPrefix
.
package main
import (
"embed"
"log"
"net/http"
"github.com/vearutop/statigz"
"github.com/vearutop/statigz/brotli"
)
// Declare your embedded assets.
//go:embed static/*
var st embed.FS
func main() {
// Plug static assets handler to your server or router.
err := http.ListenAndServe(":80", statigz.FileServer(st, brotli.AddEncoding, statigz.FSPrefix("static")))
if err != nil {
log.Fatal(err)
}
}
Error states can be handled with the staticgz.OnError
and staticgz.OnNotFound
options. These allow you to customize
the response sent to the client when an error occurs or when no resource is found.
fileServer := statigz.FileServer(
st,
staticgz.OnError(func(w http.ResponseWriter, r *http.Request, err error) {
// Handle error.
http.Error(w, err.Error(), http.StatusInternalServerError)
}),
staticgz.OnNotFound(func(w http.ResponseWriter, r *http.Request) {
// Handle not found.
http.Error(w, "Not found", http.StatusNotFound)
// Or to serve a alternative path instead you could;
//r.URL.Path = "/alternative/path"
//fileServer.ServeHTTP(w, r)
}),
)
if err := http.ListenAndServe("localhost:80", fileServer); err != nil {
log.Fatal(err)
}