perf: inline CSS to drop render-blocking request#4
Merged
Conversation
The external <link rel="stylesheet" href="assets/style.css"> was the
only render-blocking resource on every page (~110 ms per Lighthouse on
GitHub Pages). Inlining it into a <style> block in <head> drops the
critical request entirely and collapses the network dependency tree to
just the HTML document.
scripts/build.py:
- New load_inline_css() reads assets/style.css and rewrites
url("fonts/...") to url("assets/fonts/...") so the inlined font
references resolve correctly relative to the HTML document. The
rewrite matches the existing <link rel=preload as=font> hrefs, so the
preload is reused for the @font-face fetch.
- render_html() and render_index() now take an inline_css parameter
and pass it to the template.
templates/base.html.j2: replace the external <link> with
<style>{{ inline_css | safe }}</style>. Font preloads stay; favicon
stays.
The full CSS is also still copied to public/assets/style.css for any
direct consumer (PDF builds, hot-linking).
Local Lighthouse desktop preset, post-change:
- index.html 100 / 100 / 100 / 100, render-blocking savings 0 ms
- cv-executive.de 100 / 100 / 100 / 100, render-blocking savings 0 ms
- cv-technical.de 100 / 100 / 100 / 100, render-blocking savings 0 ms
Cache-TTL insight stays unaddressed: GitHub Pages hard-codes
Cache-Control: max-age=600 and we have no header override available.
Forced-reflow [unattributed] insight is not from us — pages ship zero
JavaScript.
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
There was a problem hiding this comment.
Pull request overview
This PR improves initial render performance for the generated static CV site by eliminating the render-blocking external CSS request and instead inlining the stylesheet into the HTML output, while still emitting the standalone CSS asset for non-HTML consumers (e.g., PDF generation).
Changes:
- Inline
assets/style.cssinto rendered HTML pages via a newinline_csstemplate variable. - Add
load_inline_css()to rewrite font URLs for correct resolution when CSS is embedded in<style>. - Thread
inline_cssthroughrender_html()/render_index()and update the base template accordingly.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
templates/base.html.j2 |
Replaces the external stylesheet <link> with an inlined <style> block driven by inline_css. |
scripts/build.py |
Loads and rewrites CSS for inlining, passes inline_css into all template renders, while continuing to copy assets/ to public/assets/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Lighthouse Insights on the deployed site flagged three things on top of the 100/100/100/100 categories:
assets/style.css(~110 ms LCP impact)[unattributed]— not from our code; we ship zero JSThis PR fixes (1). (2) is unfixable on GitHub Pages (Pages hard-codes
Cache-Control: max-age=600for static assets and we can't override; would need Cloudflare in front). (3) is Lighthouse's own measurement layout, not us.What
scripts/build.pygainsload_inline_css()that readsassets/style.cssand rewritesurl("fonts/...")→url("assets/fonts/...")so the inlined@font-faceURLs resolve correctly relative to the HTML document. The rewritten URLs match the existing<link rel=preload as=font>hints, so the preload is reused for the font fetch.render_html()andrender_index()take aninline_cssargument.templates/base.html.j2replaces<link rel="stylesheet" href="assets/style.css">with<style>{{ inline_css | safe }}</style>. Font preloads stay.public/assets/style.cssfor direct consumers (PDF build, anyone hot-linking).Lighthouse before vs after (local desktop preset)
/index.html/cv-executive.de.html/cv-technical.de.htmlAll four Lighthouse categories stay at 100. Network dependency tree collapses to just the HTML document (no chained CSS request).
Trade-off
CSS is now duplicated across the three HTML files (~13 KB each, ~39 KB total). Acceptable: a CV is loaded once per visitor, the LCP win matters more than caching the same CSS across pages. The CV PDF still picks up the file from
public/assets/style.cssvia WeasyPrint, so PDFs are unaffected.