Skip to content

Conversation

ElysiaFollower
Copy link

Description

This PR addresses a significant performance bottleneck in the masonry gallery. Previously, the gallery would fetch all images before rendering, causing slow initial page loads and a poor user experience, especially on image-heavy pages. This approach also lacked reliability, as a single failed image request could prevent the entire gallery from displaying.

This has been resolved by introducing a new Hexo generator that pre-fetches image dimensions at build time (hexo generate). This allows the layout to reserve the correct space for each image, eliminating content reflow (layout shift) and enabling true, high-performance lazy loading on the client-side.

Key Changes & Implementation Details

Build-Time Dimension Fetching: A new hexo generator processes all images listed in the theme's masonry configuration. (masonry.yml)

  • It supports both remote URLs and local images
  • Those can not be fetched will get a fallback size

Caching Layer: To dramatically accelerate subsequent builds, fetched dimensions are stored in a persistent JSON cache (.masonry_cache.json)

  • A new console command, hexo masonry-cache clean, is introduced to allow users to manually clear the dimension cache when needed

About local images

User convention requires local image paths in the configuration to be relative to the source directory
eg: For an image located at (your-blog)/source/images/photo.png, the configuration path in masonry.yml should be /images/photo.png.

test

Functionality has been verified in a local environment. A specific test case was designed to ensure reliability under adverse conditions.

Test Setup

  • The gallery was configured with a mix of images: some loaded from local paths and at least one from a remote URL
  • To simulate a network failure or a very slow-loading asset, one of the remote image URLs was intentionally set to an incorrect address that would never resolve.

Before (Previous Behavior)

This single failure blocked the rendering of the entire gallery. No images were displayed, even the ones that were available locally. The page appeared stuck in a "loading" state (I've waited for over 10 minutes)
PixPin_2025-08-18_21-49-35

After (With This PR)

The broken image request failed gracefully(get the fallback sizes 1:1) without impacting the rest of the page. And all other images, including the locally hosted one, rendered correctly and immediately.
PixPin_2025-08-18_21-39-08

other words

As a student, this is one of my first attempts at a larger open-source contribution. There may be some immature aspects in my implementation, and I would be very grateful for any guidance or feedback. Thank you for your time and consideration!

Previously, masonry page needs to load all images at once before displaying any content. When there were a large number of images, this would result in an excessively long page loading time

- lazy load: Images will be loaded on demand only when they scroll into the visible area (supported by lazysizes)
- Pre-read dimensions during construction: Pre-fetch the URL of the image to read its width and height, and then directly write these size information into the `<img>` tag

- more time needed for construction
- Previously, the masonry generator fetched dimensions for all gallery images on every `hexo generate` command, causing significant build delays on image-heavy sites.

- To prevent file-watcher build loops, the cache is only written to during production builds (`hexo g`) and remains read-only in development mode (`hexo s`).
Introduces a new console command to manually clear the masonry gallery's
image dimension cache.

This is necessary for users who update or replace existing images without
changing their URLs, allowing them to force a refetch of all dimensions
on the next build.
The dimension fetching generator can now process both remote URLs and
local images stored within the Hexo `source` directory.

User convention requires local image paths in the configuration to be
relative to the `source` directory (e.g., `/images/photo.png` in mansonry.yml for `/user/blog/source/images/photo.png`).
Primary Fixes:
- **Swup Navigation**: The gallery failed to render when navigated to via swup because its scripts were not re-initialized. `lazysizes` script is now loaded via `scripts.ejs`. (afraid of conflicts with existing 'lazyload.js')

-  **Windows Build Compatibility**: The glob patterns in `build.js` have been fixed to be platform-agnostic. All backslash path separators are now normalized to forward slashes to prevent parsing errors on Windows.
@ElysiaFollower
Copy link
Author

Recent Fixes (as of Aug 19, 2025)

Based on further testing, I've pushed the following fixes to address issues found in the initial implementation:

  • fix(gallery): Resolved a critical bug where the gallery's images failed to render when navigated to via swup because its scripts were not re-initialized . (I originally imported lazysizes.js only on the masonry page to avoid potential conflicts with the existing lazyload.js, but I've now moved the import to scripts.ejs)
  • fix(build): Patched a path resolution issue in the build script to ensure robust compatibility with Windows environments. (It's all of a sudden that I found my previous build was a joke. I have now fixed it on my local machine, and also tested and modified related files accordingly.)

The implementation should now be stable and ready for a full review.

@EvanNotFound
Copy link
Owner

Redefine theme shouldn't be dependent on npm package. I am not sure about your added dependency of image-size. What's your thoughts on this? Should we transition to NPM only

…etch method now, which is standard in modern Node.js versions(requires node version >=18.0.0, referencing: https://zhuanlan.zhihu.com/p/689482871).
The theme should not impose runtime npm dependencies on end-users. This commit vendors the `image-size` library by inlining its code into the theme's utilities.

This change makes the masonry gallery feature fully self-contained and improves the theme's portability and ease of use.
…size' backend dependency

why not 'js/libs':
semantically incorrect, as this directory is for frontend assets and build.js won't deal with 'image-size' folder

why not 'scripts/modules‘:
Relocated the vendored `image-size` library to `theme_modules` to isolate it from Hexo's automatic script scanning process.

This fixes the `SyntaxError` issues that occurred when the library was located within the `scripts` directory tree
@ElysiaFollower
Copy link
Author

@EvanNotFound

Redefine theme shouldn't be dependent on npm package. I am not sure about your added dependency of image-size. What's your thoughts on this? Should we transition to NPM only

Thanks so much for your guidance and feedback. You were absolutely right about avoiding runtime dependencies, and I appreciate you pointing me in the right direction. My apologies for the initial oversight.

I've since refactored the entire implementation based on your question. The feature is now fully self-contained and no longer adds any runtime npm dependencies to the theme.

Here’s a summary of the key changes:

  • Created a theme_modules Directory: I've added a theme_modules directory at the theme's root. This serves as a dedicated location for housing internal, backend-only libraries like image-size, keeping them cleanly separated from the frontend build process and Hexo's script loader.
  • Replaced node-fetch with Native fetch: The node-fetch dependency has been completely removed in favor of the native fetch API, which is standard in modern Node.js.
    • Note on Compatibility: Native fetch introduces a requirement for Node.js v18.0.0 or higher. I believe this is a reasonable baseline for modern projects, but please let me know if you have a different policy for Node version support. If supporting older Node.js versions is a priority, I'm more than happy to revert this change and vendor the node-fetch@2 library into theme_modules instead.

Apologize again for my immature initial oversight. I should have studied the project's architecture more closely before submitting. It's my fault and I truly appreciate you taking the time to guide me through the correct approach.

@EvanNotFound
Copy link
Owner

EvanNotFound commented Sep 17, 2025

Thanks for your PR. I really appreciate your help.

However I would like you to not use ChatGPT to reply to me in PR. When opensource clarity is the most important, as we don't care about how correct your grammar is or how polite you are.

You can just tell me directly what you want to say.

用中文也行。

感谢

@EvanNotFound

Redefine theme shouldn't be dependent on npm package. I am not sure about your added dependency of image-size. What's your thoughts on this? Should we transition to NPM only

Thanks so much for your guidance and feedback. You were absolutely right about avoiding runtime dependencies, and I appreciate you pointing me in the right direction. My apologies for the initial oversight.

I've since refactored the entire implementation based on your question. The feature is now fully self-contained and no longer adds any runtime npm dependencies to the theme.

Here’s a summary of the key changes:

  • Created a theme_modules Directory: I've added a theme_modules directory at the theme's root. This serves as a dedicated location for housing internal, backend-only libraries like image-size, keeping them cleanly separated from the frontend build process and Hexo's script loader.

  • Replaced node-fetch with Native fetch: The node-fetch dependency has been completely removed in favor of the native fetch API, which is standard in modern Node.js.

    • Note on Compatibility: Native fetch introduces a requirement for Node.js v18.0.0 or higher. I believe this is a reasonable baseline for modern projects, but please let me know if you have a different policy for Node version support. If supporting older Node.js versions is a priority, I'm more than happy to revert this change and vendor the node-fetch@2 library into theme_modules instead.

Apologize again for my immature initial oversight. I should have studied the project's architecture more closely before submitting. It's my fault and I truly appreciate you taking the time to guide me through the correct approach.

@EvanNotFound
Copy link
Owner

However, by including the whole image-size library into redefine theme is not a viable approach.

As your first PR, this is really good work.

But on a more practical level, I would say you will need a bit more experience on how to deal with problems like this.

My approach will be rethink how we should handle the images that don't load, maybe just remove them completely, instead of depending on a library. Or, we can just write a custom function to get the size of that image.

Depends on you. The current state of this PR is not mergeable. Sorry.

… not all).

Based on  ITU-T T.81 standard. Application formats like JFIF and EXIF are supported. Correctness of application format that doesn't follow T.81 standard is not guaranteed
…n imageSize inside now.

test good on over a hundred images. including 'png'/'jpeg'/'gif'. where 'png' and 'jpeg' can get size automatically and 'gif' cann't, but 'gif' size can be given in `masonry.yml` for gallery
@ElysiaFollower
Copy link
Author

@EvanNotFound
非常感谢您的包容理解与指导。最初使用AI对语言润色是出于严谨性和专业性的考量,没想到反而给您带来了困扰,那么现在我将以我最舒适的表达开诚布公地convey我所有的想法。

问题定位

首先,关于这个PR针对的问题,实际上指的是——原来相册页面必须等到所有图片全部获取之后才会进行渲染,而当图片数量足够多,以至于全部获取时间难以忍受的时候,这个页面也会迟迟渲染不出来,令人难受。我虽然设计了一个“某图片无法获取”的案例,但那其实是为了“模拟” 【获取全部图片的时间难以忍受】的场景,此时就是无穷大。而即便所有图片的资源获取本身都没有问题,当数量足够多的时候,仍然会趋于这种情况。这也是我认为这个修改是perf而不是fix的根本原因。
所以我认为您提到的“handle the images that don't load, maybe just remove them completely” 这样的方法并不能从根本上解决我希望解决的问题。我思索之后仍然认为,实现该页面的懒加载是有意义的。
只不过为了同时实现懒加载和瀑布流布局,我们需要预读图像尺寸,为了在预读时实现对图片的解析并获取尺寸,我先前选择了现有库。在我看来这有一种好处,那就是维护和更新成本几乎全部转嫁到了对应库的维护者。不过您的话也给我带来了很多启发,我仔细思考了一下的确认为,这个功能在现有场景下是有妥协解的——用不到10%的可维护的安全的代码量,实现超过90%的功能 —— 所以我现在尝试自行实现了imageSize()方法,提供对PNG和基于T.81标准JPEG图像尺寸的解析。

这次更改

现在我已经完成了我的imageSize()函数,并移除了对image-size库的依赖。您可以审阅。
关于其中涉及到的规范标准,我简单写了一点总结,这份总结就是我设计imageSize()函数的基石,如果您觉得审查有困难的话可能可以参考着看看
设计基于的权威资料:
PNG: 基于2025 年 6 月 25 日发布的第三版 PNG 规范
JPEG: 基于T.81标准(又名ISO/IEC 10918-1 标准)

关于为什么我认为只提供PNG和JPEG主流标准支持是可行的

据我自己的观察,当前互联网上流通的绝大部分图像都是PNG和JPEG格式,至少我自己手上的图像绝大多数全部这两种格式。其中JPEG虽然似乎有很多变体,但是主流的仍然是基于T.81标准的。—— 线索1: 该函数已经能够实现对绝大多数图像的自动解析
同时,我在图像预读的时候,是支持用户在masonry.yml配置文件,即配置相册图像的那个配置文件,额外为图像添加width和height字段(不添加也没事), 如果添加的话,那么预读尺寸时将会优先采用该尺寸,而不会再去进行图像的fetch和解析。基于此原理,即便是不支持的图像格式如webp、gif,都可以正常处理。 —— 线索2: 有fallback手段,理论上任何格式的图像都仍然可以正常布局
总结:绝大多数图像都能自动处理,极少部分不能处理的,也可以手动标记(而且很方便)

测试

我上传了我一个相册文件夹里的所有图像,超过100张,涵盖PNG、JPEG、GIF格式。相册页面可以正常渲染显示。值得一提的是,这里涉及到的所有JPEG图像都没有弹出Unsupported image format提示,全部能够自动解析,这进一步说明了,当前的imageSize实现能够支持绝大部分的图像。少量的GIF通过配置width,height标签就能正常显示,配置格式大致如下, 保持了原有风格:

关于缓存

关于为什么要实现缓存机制,我想我可能也需要解释一下:问题的根本是获取一次全部图像需要的时间长到难以忍受。即便通过预读的设计,能够将读取的时间从浏览转移到构建,但这还不够,每次构建都要额外等这么久获取图像太难熬了。考虑到图像的尺寸是一个静态不变的,而且相册配置大概率也会是处于一个不常大规模变动的设置,将已经读取到的尺寸进行可持久化存储,就能大大减少构建时的等待时间。理想情况下,每张图片只会被读取一次,均摊时间复杂度是1,即便第一次hexo g可能花点时间,但我认为也完全可以接受。
不过需要一提的是,在我的设计中,hexo s是不会触发缓存可持久化的,因为文件的变动可能导致hexo s内部再次触发构建过程,从而导致死循环。所以如果用户只hexo s不hexo g...... 那么缓存机制对他来说是几乎不存在的,不过我认为这种情况不太可能发生..

关于fetch

这一方面其实我还琢磨不太透。为了移除依赖,我是使用了node原生的fetch,我个人粗浅地认为这是件好事,毕竟官方支持了fetch,无论是出于安全性还是稳定性的考量,都没道理不用。但是问题在于,node v18.0.0及之后才稳定支持原生fetch。但是我瞟了一眼发现该项目node依赖的最低要求是v12.0。疑惑之余(毕竟node都出到v22了...), 也是很纠结,难道对于fetch这么基础性的方法也要自行实现一遍以兼容低版本的node吗?

闲话

因为近期开学了,琐事颇多,所以回复可能稍微有些慢。但请您相信,我仍然愿意为实现的不好的地方做出修改。您有任何不满意的地方请尽管开口说,即便水平有限,但既然发起了这次PR我就会为之负责。

…mely 0xffffff...); can not distinguish data 0xff00 from marker; can not deal with RST_m markers which do not length.

now it can. Improvement of robustness
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants