Skip to content

feat: lazy-load non-critical vendor CSS - #965

Closed
pivaldi wants to merge 1 commit into
next-theme:masterfrom
pivaldi:feat/lazy-css
Closed

feat: lazy-load non-critical vendor CSS#965
pivaldi wants to merge 1 commit into
next-theme:masterfrom
pivaldi:feat/lazy-css

Conversation

@pivaldi

@pivaldi pivaldi commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Motivation

FontAwesome, Fancybox and KaTeX CSS are render-blocking but not needed for
first paint. Loading them eagerly hurts Lighthouse / Core Web Vitals (FCP, LCP)
on pages that don't use them above the fold. This makes those stylesheets
non-render-blocking via opt-in lazy loading.

Changes

  • New opt-in config (_config.yml), disabled by default so behavior is
    unchanged unless enabled:

    # Performance optimization
    performance:
      # Lazy-load non-critical CSS (FontAwesome, Fancybox, KaTeX)
      # This improves Lighthouse scores by making these CSS files non-render-blocking
      lazy_css: false
  • next_vendors helper (scripts/helpers/engine.js) accepts an options
    object with a lazy flag. When lazy is set and performance.lazy_css
    is enabled, the stylesheet is loaded with the standard preload + onload
    swap technique, plus a <noscript> fallback for JS-disabled clients:

    <link rel="preload" href="" as="style" onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href=""></noscript>

    JS handling is unchanged (it already uses defer). Existing integrity /
    crossorigin attributes are preserved in both the preload and noscript tags.

  • Templates opt the three non-critical stylesheets in:

    • layout/_partials/head/head.njkfontawesome, fancybox_css
    • layout/_third-party/math/katex.njkkatex

Notes

Testing

  • performance.lazy_css: false (default): tags render exactly as before
    (render-blocking <link rel="stylesheet">).
  • performance.lazy_css: true: FontAwesome / Fancybox / KaTeX CSS emit the
    preload + onload swap with a <noscript> fallback; styles still apply
    with and without JavaScript.

@CLAassistant

CLAassistant commented Jun 27, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

Copy link
Copy Markdown

This pull request contains changes to the configuration file. Please make sure the documentation in NexT website is changed or added.
Please edit relevant source files here: https://github.com/next-theme/theme-next-docs/tree/master/source/docs and create a pull request with the changes here: https://github.com/next-theme/theme-next-docs/pulls

@stevenjoezhang

stevenjoezhang commented Aug 13, 2026

Copy link
Copy Markdown
Member

@pivaldi Thanks for splitting this change into a separate PR. One concern is that this reintroduces an inline event handler:

onload="this.onload=null;this.rel='stylesheet'"

NexT previously removed this pattern specifically to support CSP without unsafe-inline:

onload here has the same CSP behavior as the onclick attributes removed by #303. With a strict script-src policy, the browser blocks this handler, so the link remains rel="preload" and the stylesheet is never applied. The <noscript> fallback does not help because JavaScript is still enabled.

Could we keep the lazy-loading logic in an external script instead? The smallest change would be to mark these links with a data attribute and let some deferred js code change their rel:

<link rel="preload" href="" as="style" data-next-lazy-css>
<noscript><link rel="stylesheet" href=""></noscript>
document.querySelectorAll('link[data-next-lazy-css]').forEach(link => {
  link.rel = 'stylesheet';
});

This avoids introducing another script request, preserves the no-JavaScript fallback, and does not require unsafe-inline. Since the preload starts while the document is being parsed and config.js is already deferred, the stylesheet request can still begin early without relying on an inline handler.

@stevenjoezhang

Copy link
Copy Markdown
Member

Separately, for users mainly concerned about the Font Awesome loading speed, I created next-theme/hexo-next-fontawesome-subset. It scans the final Hexo routes and generates site-specific subset WOFF2 files containing only the Font Awesome glyphs actually used by the site, while preserving the existing fa-* class API and the official all.min.css.

@stevenjoezhang

Copy link
Copy Markdown
Member

Thanks for working on this and for splitting the CSS delivery change into a focused PR.

During review, I realized that the NexT organization already maintains this functionality in hexo-optimize. Its filter_optimize.css.delivery option allows users to select individual stylesheets for asynchronous delivery during post-generation:

filter_optimize:
  css:
    delivery:
      - font-awesome
      - fontawesome-free
      - fancybox
      - katex

We think CSS delivery is better handled as a generic build optimization rather than as theme-specific template behavior. Keeping it in hexo-optimize has several advantages:

  • It works with any Hexo theme.
  • Users can select resources individually instead of enabling one switch for Font Awesome, Fancybox, and KaTeX together.
  • It avoids adding another performance configuration and helper API to NexT.
  • The optimizer can apply the transformation consistently to generated HTML.

Selective control is also useful because Font Awesome and KaTeX may be required above the fold on some sites, where delaying them could cause FOUC or layout shifts.

This review also exposed an issue in the current hexo-optimize implementation: it uses the same inline onload technique, which conflicts with strict CSP and NexT’s previous work in #220 and #303 to remove inline event handlers. We plan to address the concern in the optimizer instead of introducing a second implementation in the theme. The intended design is to preserve existing link attributes such as integrity and crossorigin, mark selected links with a data attribute, retain a noscript fallback, and activate them from an external loader script.

For these reasons, we do not plan to merge this PR. This is not a reflection on the effort or quality of the contribution. It helped us identify both the overlap and an improvement needed in our existing optimizer. Thank you again for the proposal and the careful explanation of the performance motivation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants