fast_html.py (2420B)
1 """Make rendered HTML pages cheap to reuse. 2 3 Every UI page is rendered from a template and is identical for everybody (it only carries global 4 stats and URLs), so two things are safe and make navigation much faster: 5 6 * ``Cache-Control: private, max-age=30``: the browser keeps the page for 30 seconds. The link 7 prefetcher (static/prefetch.js) fetches pages in the background, and a click on one of them is then 8 answered from the browser's own cache with no network at all. ``private`` keeps shared caches out of it. 9 * gzip: the pages are 20-90 KB of HTML with inlined CSS/JS and compress about 5x, which matters most on 10 slow or high-latency connections. 11 12 Only successful GET responses built through ``TemplateResponse`` are touched. File downloads, the API, 13 redirects, errors and the admin dashboard never go through here. 14 """ 15 16 import gzip 17 18 from fastapi.templating import Jinja2Templates 19 from starlette.requests import Request 20 21 HTML_MAX_AGE = 30 # seconds; keep in step with `ttlMs` in static/prefetch.js 22 MIN_GZIP_BYTES = 1024 23 GZIP_LEVEL = 5 24 25 26 def optimise_html(request: Request | None, response): 27 """Add caching and compression headers to a rendered page (in place) and return it.""" 28 if request is None or request.method != "GET" or response.status_code != 200: 29 return response 30 31 if "cache-control" not in response.headers: 32 response.headers["Cache-Control"] = f"private, max-age={HTML_MAX_AGE}" 33 34 # The body now depends on Accept-Encoding, so caches must key on it. 35 response.headers.append("Vary", "Accept-Encoding") 36 37 body = response.body 38 accepts_gzip = "gzip" in request.headers.get("accept-encoding", "").lower() 39 if accepts_gzip and "content-encoding" not in response.headers and len(body) >= MIN_GZIP_BYTES: 40 compressed = gzip.compress(body, GZIP_LEVEL) 41 response.body = compressed 42 response.headers["Content-Encoding"] = "gzip" 43 response.headers["Content-Length"] = str(len(compressed)) 44 return response 45 46 47 class FastTemplates(Jinja2Templates): 48 """Jinja2Templates whose responses are cached and compressed (see module docstring).""" 49 50 def TemplateResponse(self, *args, **kwargs): # noqa: N802 (matches the parent's name) 51 response = super().TemplateResponse(*args, **kwargs) 52 request = args[0] if args and isinstance(args[0], Request) else kwargs.get("request") 53 return optimise_html(request, response)