Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Highlight with Prism in a worker and gzip text responses

306f36fcef89c145abcd0c5331453d0b179f37ce · Keenan Nemetz · 12d ago · parent e5542ac6

Highlight with Prism in a worker and gzip text responses

The code browser now names a blob's language and lets the browser colour it.
Chroma is gone: server/web/highlight.go is an extension table, and
static/prism-worker.js runs Prism — every language Prism ships — off the main
thread. The server sends escaped lines with their numbers and L# anchors, so a
page is complete and scrollable before any script runs and the worker only swaps
the contents of each line cell. The table covers 186 languages; a file it does
not claim stays plain rather than being coloured by a guess.

The bundle those grammars make is 545 KB, so the instance now gzips text
responses itself instead of relying on a proxy, and static assets carry a
content ETag so a browser revalidates with a 304 rather than re-downloading the
bundle on every code page. The raw-download route opts out with
Cache-Control: no-transform because it promises an exact Content-Length.

This also makes JavaScript a requirement rather than a nicety: the label and
assignee token inputs lose their noscript save buttons and their comma-separated
text fallback, and the docs say so.

Measured: release binary 26.08 MB -> 22.70 MB; /r/{repo}/code 11.1 KB -> 2.6 KB
over the wire; the Prism bundle 545 KB -> 206 KB gzipped, fetched once.

scripts/vendor-prism.py regenerates the bundle from Prism's components.json
require graph, and --check fails if the committed assets drift.

server/web/compress.go +188

@@ -0,0 +1,188 @@
1 package web
2
3 // gzip response compression.
4 //
5 // The web surface sends HTML, CSS, JavaScript and source blobs, all of which
6 // compress to a fraction of their size: the vendored Prism bundle alone is
7 // 545 KB and lands near 200 KB gzipped, and a rendered page about a fifth of its
8 // size. There is no brotli here on purpose — it would add a dependency for a few
9 // points of ratio, and Go's gzip needs none.
10 //
11 // The rules are narrow, because the same handler chain also streams git
12 // responses, release archives and LFS objects:
13 //
14 // - only when the client sent `Accept-Encoding: gzip` with q > 0;
15 // - only text-like content types, never a body that is already encoded;
16 // - never a Range request (the compressed body has no meaningful ranges);
17 // - never a body the handler has already sized below compressMinBytes;
18 // - never a 1xx, 204, 206, 304 or redirect (nothing to gain, and 304 must
19 // stay bodiless).
20 //
21 // The writer is transparent otherwise: it passes Header through, keeps Flush
22 // working for the streaming handlers, and exposes Unwrap so a handler can reach
23 // the underlying ResponseWriter.
24
25 import (
26 "compress/gzip"
27 "net/http"
28 "strconv"
29 "strings"
30 )
31
32 // compressMinBytes is the smallest declared body worth a gzip header and
33 // trailer. Below it the compressed form is usually larger than the source.
34 const compressMinBytes = 256
35
36 // compressor decides per response whether to gzip, and lazily starts the stream
37 // on the first write so a header-only response never opens one.
38 type compressor struct {
39 rw http.ResponseWriter
40 r *http.Request
41 gz *gzip.Writer
42 status int
43 wroteHeader bool
44 gzipping bool
45 }
46
47 func newCompressor(rw http.ResponseWriter, r *http.Request) *compressor {
48 return &compressor{rw: rw, r: r}
49 }
50
51 func (c *compressor) Header() http.Header { return c.rw.Header() }
52
53 func (c *compressor) WriteHeader(status int) {
54 if c.wroteHeader {
55 return
56 }
57 c.wroteHeader = true
58 c.status = status
59 h := c.rw.Header()
60 if c.acceptsGzip() && compressibleStatus(status) && compressibleHeader(h, c.r) {
61 c.gzipping = true
62 h.Set("Content-Encoding", "gzip")
63 h.Del("Content-Length")
64 h.Add("Vary", "Accept-Encoding")
65 }
66 c.rw.WriteHeader(status)
67 }
68
69 func (c *compressor) Write(b []byte) (int, error) {
70 if !c.wroteHeader {
71 c.WriteHeader(http.StatusOK)
72 }
73 if !c.gzipping {
74 return c.rw.Write(b)
75 }
76 if c.gz == nil {
77 c.gz = gzip.NewWriter(c.rw)
78 }
79 return c.gz.Write(b)
80 }
81
82 // Flush keeps streaming handlers working: the header is settled, the gzip
83 // buffer is emptied, and the underlying writer is flushed if it can be.
84 func (c *compressor) Flush() {
85 if !c.wroteHeader {
86 c.WriteHeader(http.StatusOK)
87 }
88 if c.gz != nil {
89 _ = c.gz.Flush()
90 }
91 if f, ok := c.rw.(http.Flusher); ok {
92 f.Flush()
93 }
94 }
95
96 // Close finishes the gzip stream. It is called once by the middleware after the
97 // handler returns, never by a handler, but it is idempotent enough for the
98 // panic path that may have written nothing at all.
99 func (c *compressor) Close() {
100 if c.gz != nil {
101 _ = c.gz.Close()
102 c.gz = nil
103 }
104 }
105
106 func (c *compressor) Unwrap() http.ResponseWriter { return c.rw }
107
108 // acceptsGzip reports whether the client asked for gzip and would accept it:
109 // `gzip` with a q-value of zero is a refusal, not a request.
110 func (c *compressor) acceptsGzip() bool {
111 if c.r.Method == http.MethodHead {
112 return false
113 }
114 for _, part := range strings.Split(c.r.Header.Get("Accept-Encoding"), ",") {
115 name, params, _ := strings.Cut(strings.TrimSpace(part), ";")
116 if !strings.EqualFold(strings.TrimSpace(name), "gzip") {
117 continue
118 }
119 q := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(params), "q="))
120 if q == "" {
121 return true
122 }
123 if v, err := strconv.ParseFloat(q, 64); err == nil {
124 return v > 0
125 }
126 return true
127 }
128 return false
129 }
130
131 // compressibleStatus covers the bodies worth compressing: a rendered page, a
132 // fragment, and the styled error page. Redirects are a sentence, a 304 must stay
133 // bodiless, and a 206 is already a byte range.
134 func compressibleStatus(status int) bool {
135 switch status {
136 case http.StatusNoContent, http.StatusNotModified, http.StatusPartialContent:
137 return false
138 }
139 return (status >= 200 && status < 300) || status >= 400
140 }
141
142 // compressibleHeader applies the remaining rules: a body that is already
143 // encoded stays as it is, a Range request is answered with bytes, and a body
144 // the handler already sized as tiny is not worth compressing.
145 func compressibleHeader(h http.Header, r *http.Request) bool {
146 if h.Get("Content-Encoding") != "" || h.Get("Content-Range") != "" {
147 return false
148 }
149 // A handler that promised exact bytes (a raw download with its Content-Length)
150 // opts out with the standard directive rather than a private header.
151 if strings.Contains(strings.ToLower(h.Get("Cache-Control")), "no-transform") {
152 return false
153 }
154 if r.Header.Get("Range") != "" {
155 return false
156 }
157 if n, err := strconv.Atoi(h.Get("Content-Length")); err == nil && n < compressMinBytes {
158 return false
159 }
160 return compressibleType(h.Get("Content-Type"))
161 }
162
163 // compressibleType is the allow-list of types worth gzipping. Anything else —
164 // images, archives, packs, LFS objects, git protocol output — is already
165 // compressed or is a byte range where compression cannot help.
166 func compressibleType(contentType string) bool {
167 ct := strings.ToLower(strings.TrimSpace(contentType))
168 if i := strings.IndexByte(ct, ';'); i >= 0 {
169 ct = strings.TrimSpace(ct[:i])
170 }
171 switch ct {
172 case "text/event-stream":
173 return false
174 case "image/svg+xml",
175 "application/json",
176 "application/javascript",
177 "application/x-javascript",
178 "application/xml",
179 "application/xhtml+xml",
180 "application/manifest+json",
181 "application/rss+xml",
182 "application/atom+xml":
183 return true
184 }
185 return strings.HasPrefix(ct, "text/") ||
186 strings.HasSuffix(ct, "+json") ||
187 strings.HasSuffix(ct, "+xml")
188 }