Performance Optimization
This page explains the blog’s performance optimization measures: image lazy loading, asset minification, CDN acceleration, image format optimization, and LCP (largest contentful paint) optimization principles. Most optimizations work out of the box — the key is understanding the layered strategy and not breaking it when adding resources.
1. Lazy Loading (lazyload)
Purpose: images and comment plugins outside the visible area aren’t loaded in advance, reducing first-paint requests and traffic. Config lives in the theme config (edit userConfig/_config.tmp.yml for production):
lazyload:
enable: true # true | false
loading_img: /medias_webp/loading2.svg # loading placeholder (light)
loading_img_dark: /medias_webp/loading2-dark.svg # placeholder (dark)
onlypost: false # true = lazy loading only on post pages
offset_factor: 2 # trigger offset multiplier (base = viewport height)- Add the
no-lazyclass to skip lazy loading (e.g. first-screen banner) - With
onlypost: falseit applies site-wide; standalone pages can opt in withlazyload: truein front-matter - Tune
offset_factorby deployment request speed: larger = loads earlier (lazy loading starts closer to the bottom)
2. Asset Minification (hexo-minify)
Purpose: minify JS/CSS/HTML at build time to reduce size. Root config _config.yml:
minify:
preview: false ## whether to minify during local preview (false = minify in production)
exclude: ['*.min.*'] # skip already-minified files
js: { enable: true } # JS minification
css: { enable: true } # CSS minification
html:
enable: true
options:
minifyJS: true # minify inline JS in HTML
minifyCSS: true # minify inline CSS in HTML
removeComments: true # strip comments
collapseWhitespace: true # strip extra whitespace
removeAttributeQuotes: true
image:
enable: false # image minification off by default (images have their own optimization, see section 4)Notes:
preview: falsemeans localhexo serverdoesn’t minify; verify minification withnpm run build(see the Deployment chapter)- Compression is toggled via
# minify-switchmarker lines in the root_config.yml;tools/cicd.shsed-switches them per build mode (-doff,-r/-c/-ton), then restores them automatically after the build
3. CDN Acceleration
Purpose: static assets are served from jsDelivr’s global nodes, fetched from the nearest edge. The mechanism is placeholder injection: templates contain {cdnPathVersion}, replaced with real CDN URLs at CI build time (see the CDN section of the Global Configuration chapter).
# theme config
jsDelivr:
url: {cdnPathVersion} # versioned CDN path (replaced at build time)
latesturl: #{cdnPathLatest} # latest path (commented out, not enabled)
cdn:
url: "{cdnUrl}" # JS/CSS CDN URL
mediaUrl: "{mediaUrl}" # image/media CDN URL
resUrl: "{resUrl}" # resource CDN URL- The version number is the key:
-rreleases bumptools/.VERSIONand inject{urlVersion}(e.g.?v={urlVersion}) so CDN and browser caches fetch new files; content-only-cdoesn’t bump — CDN hits the old version, which is fine since content is unchanged - After release
tools/purge-cdn.sh -aflushes CDN cache (auto-called in cicd.sh release mode) - Images use a separate CDN domain (
{mediaUrl}), separated from JS/CSS for independent cache control
4. Image Optimization (webp)
Purpose: images are the biggest chunk of this blog’s size. Site images are uniformly webp, far smaller than jpg/png, and distributed via CDN. webp files live in the theme source themes/matery/source/medias_webp/ (copied to public/medias_webp/ at build time; URLs use /medias_webp/...):
# example: the featureimages library is all webp
/medias_webp/featureimages/0.webp
/medias_webp/avatar.webp
/medias_webp/loading2.svg # SVG placeholder (vector, tiny)- Convert new images to webp before committing them: the image repo is the submodule
userConfig/cloudimg/(synced bytools/processRepo.sh);imgurl.shprocesses URLs at build time - Image loading follows section 1 lazy loading + section 3 CDN distribution, both layers active
hexo-minify’simage.enableis off by default — in-site webp is already the final form, no second compression needed
5. LCP Optimization Principles (JS Layering)
Purpose: LCP (Largest Contentful Paint) is the key first-screen metric. A JS loading strategy layering refactor was completed on 2026-08-11; the principle is first-screen essentials synchronous, the rest defer, heavy items on demand:
| Layer | JS | Loading | Rationale |
|---|---|---|---|
| L0 critical sync | utils + colorSchema | synchronous (in head) | global namespace dependencies, needed for first screen |
| L1 defer | materialize/masonry/imagesloaded/aos/events/plugins/lightgallery/search engines | defer | parallel download without blocking parsing; defer preserves execution order |
| L2 on-demand dynamic | typed/mermaid/mathjax/echarts/tocbot | conditional dynamic injection | zero cost on first screen |
| L3 async | umami/busuanzi etc. analytics | async | never blocks rendering |
Three disciplines when adding your own JS:
deferby default:<script src="..." defer></script>, never block synchronously- Don’t touch the two L0 files: utils/colorSchema are the global namespace foundation; sync loading is a design decision
- Heavy libraries load on demand: mermaid/mathjax/echarts only appear on specific pages and shouldn’t be in the site-wide scripts — inject them on conditional trigger
Verification:
# build output check: script tags generated by scripts.ejs should carry defer
grep -rl defer public/js/ # or inspect the script tags in public/index.htmlMeasured data shows LCP dropped from 4.2s to 1.8s (57% reduction) after optimization.
5.1 jQuery Removal & Script Layering Details (P4/P5 site-wide complete 2026-09-05)
The theme has removed jQuery site-wide, switching to native DOM APIs (querySelector / addEventListener / classList / getComputedStyle etc.). Current status of jQuery-related items in themes/matery/_config.yml libs.js.* (sources: themes/matery/_config.yml, themes/matery/layout/_partial/common/scripts.ejs):
| Config item | Status | Reason |
|---|---|---|
libs.js.jquery | Disabled (commented out) | Comment: “2026-09-05 disabled: P4/P5 site-wide jQuery removal complete” |
libs.js.jqueryUI | Config key retained (_config.yml not commented), but the <script> in scripts.ejs is commented out → not actually loaded | key still present in _config.yml |
libs.js.jqueryBarrager | Disabled (commented out) | Disabled after contact page removal (jQuery barrage deprecated) |
libs.js.jqueryPjax | Config key retained | No references remain in layout/, scripts/, or source/js/ |
As a result, the L0 layer in the §5 table no longer includes jquery — the synchronous layer contains only
utils.js+color-schema.js.
Critical inline scripts must precede defer (sources: head.ejs, scripts.ejs):
head.ejssynchronously loadsutils+colorSchemaand executesexport_config()(injectswindow.CONFIG) — these must come before alldeferscripts so they can read the global namespace/config.scripts.ejsmultilingual configMATERY_LANGand i18n copyMATERY_I18Nare synchronous inline<script>tags, deliberately placed beforewiki.js(defer); the source comment reads “injected as synchronous script, guaranteed to execute before all defer scripts”.
Loading method classification:
defer: materialize / masonry / imagesloaded / aos / events / plugins / search / scrollProgress / lightgallery / boot;instantpageistype="module"(module already implies defer; nodeferattribute added).async: umami / busuanzi / tagCommon (analytics and comment helpers, non-render-blocking).- Conditional (specific pages/layouts only): typed / mermaid / mathjax / echarts / tocbot.
JS compatibility pitfall (2026-09-22): classList.add('a b') with a space-separated multi-class string throws InvalidCharacterError (DOMTokenList rejects spaces) — use classList.add('a', 'b') or split and add individually. The homepage once failed this way.
6. Page Preloading (instantpage)
Purpose: preload the target page when the user hovers a link, making clicks feel instant.
Config (theme config):
instantpage:
enable: trueNotes: based on instant.page; enable: true applies site-wide; on mobile (no hover) it degrades gracefully without preloading.
7. Dark Feature Images (darkFeatureImages)
Purpose: feature image library for dark mode — the dark counterpart of featureimages, used for random picks in dark mode.
Config (theme config):
darkFeatureImages:
- /medias_webp/featureimages/dark/0.webp
- /medias_webp/featureimages/dark/1.webp
- /medias_webp/featureimages/dark/2.webp
# ... more dark imagesNotes: corresponds to the light featureimages library; in dark mode, banners/covers pick randomly from this list; images live in themes/matery/source/medias_webp/featureimages/dark/.
8. Image Size Plugin (image_size_plugin)
Purpose: automatically add width/height attributes to images at build time to prevent layout shift (CLS); supports mapping local images to online CDN paths.
Config (theme config):
image_size_plugin:
enable: true
proxy_url: # image proxy address (e.g. http://xray:10089)
debug: true # true | false print switch
onlypost: false # true = only process post images
# local image paths (array: theme assets + site post images)
local_path:
- themes/matery/source
- source
# local path index for mapping to online paths
map_paths:
- userConfig/cloudmedia
- userConfig/cloudimg
- themes/matery/source
# local-to-online path mapping
map_paths_web_root:
userConfig/cloudmedia: https://cdn.17lai.site
userConfig/cloudimg: https://cimg1.17lai.siteNotes: skips images with the no-size class or existing width/height attributes; map_paths + map_paths_web_root replace local image paths with online CDN URLs (images in the submodule repos userConfig/cloudimg and userConfig/cloudmedia go through this mapping).
9. CSS Async + SEO (2026-09-07)
9.1 Non-critical CSS Async Loading
Animation libraries (aos/animate.css) are not critical above the fold. Load them async with media="print" onload="this.media='all'" to eliminate render-blocking:
<link rel="stylesheet" href="..." media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="..."></noscript>Note: When rel="stylesheet preload" and a regular stylesheet point to the same URL, browsers auto-deduplicate (no double download) — no need to remove either.
9.2 SEO Structured Data (JSON-LD)
The theme ships layout/_partial/common/structured-data.ejs, auto-injected into every page:
- WebSite (all pages): site name + SearchAction (sitelinks searchbox rich result)
- BlogPosting (post pages): headline/description/date/author/cover/keywords/category
- Person + Organization: cross-referenced via
@id(avoids duplicate data)
All data comes from existing variables (config.title/url/author/language + page.*) — no manual maintenance. Validate with Google Rich Results Test (after production deploy).
9.3 Social Share Image (og:image)
# Theme config (must also sync userConfig/_config.tmp.yml)
open_graph:
default_og_image: /medias_webp/featureimages/15.webpPriority: page.img (post image) → theme.default_og_image (default) → theme.featureImages[0]. Post pages use the post cover; home/list pages use the default image.
9.4 LCP Element Priority
Above-the-fold images like the logo carry fetchpriority="high" (built into header/mobile-nav). Note: the home banner background is JS-injected (random image rotation) — do NOT change it to static CSS, that would break the random rotation feature.
Appendix: Performance Optimization Quick Reference
| Measure | Config location | Key points |
|---|---|---|
| Lazy loading | theme config lazyload | no-lazy class skips; onlypost controls scope |
| Asset minification | root _config.yml minify | local preview not minified; -d disables |
| CDN | theme config jsDelivr/cdn | -r bumps the version to flush caches |
| Images | /medias_webp/ + CDN | uniform webp; image repo is a submodule |
| LCP layering | layout/_partial/common/scripts.ejs | L0 sync / L1 defer / L2 on-demand / L3 async |
| CSS async | layout/_partial/common/css.ejs | aos/animate via media="print" onload |
| SEO structured | layout/_partial/common/structured-data.ejs | auto JSON-LD WebSite + BlogPosting |
| Social share image | theme config default_og_image | fallback default image for home/list (also sync userConfig/_config.tmp.yml) |
| LCP element | header/mobile-nav logo img | fetchpriority="high" |