welpo / tabi

A modern Zola theme with search, multilingual support, optional JavaScript, a perfect Lighthouse score, and a focus on accessibility.
https://welpo.github.io/tabi/
MIT License
97 stars 32 forks source link

Long build times - what could be the reason? #327

Closed stalkerGH closed 1 week ago

stalkerGH commented 3 weeks ago

I'd like to know how can I check the reason of long build time of my webpage created with tabi theme. It has only 12 pages but build time is about 15 seconds. Adding every 1 page (I have several drafts) extends this time by ~1 second.

I have also another blog converted from Wordpress to Markdown but with zolarwind theme. It contains 73 pages and builds in 2 seconds.

I use latest Zola (0.18.0) and latest tabi (I hope).

welpo commented 3 weeks ago

Hi!

Please ensure you're using the latest version, as a recent update (#325) greatly improved build times.

Let me know whether you're indeed using tabi with the fix, please.

stalkerGH commented 3 weeks ago

Yes, I got the latest version, with changes in templates/macros/settings.html.

welpo commented 3 weeks ago

Thanks for confirming.

Is it using image processing macros? Those tend to increase build times.

stalkerGH commented 3 weeks ago

Hmmm, I don't know... How can I check it? Most of my images are .webp plus some .png.

welpo commented 3 weeks ago

Are you using the image shortcodes? Or image resizing?

stalkerGH commented 3 weeks ago

Yes, I'm using image shortcodes, mostly image_toggler and full_width_image. Some images have full_width and inline. Do you think this is the reason?

welpo commented 3 weeks ago

Image processing is costly. I'm not finding the exact discussion where I learnt this, but this is related: https://github.com/getzola/zola/issues/2106

Do you think this is the reason?

It's likely. Try to remove the image shortcodes and we'll see the difference.

stalkerGH commented 3 weeks ago

Thanks for advice. I have plenty of images (~100 currently). So maybe it is the case. I'll try to mark some pages as draft and try to build page.

welpo commented 3 weeks ago

Let me know how it goes!

stalkerGH commented 3 weeks ago

I disabled (by draft = true) two pages with about 30 images each and build time dropped from 15 to 5 seconds. So it's not a bug - the images are "the problem".

Thank you!

stalkerGH commented 3 weeks ago

My "problem" is solved so I close the issue.

welpo commented 3 weeks ago

I'm glad you figured it out! Thanks for reporting back.

If build time is important, you could try using png instead of webp; the Zola discussion I linked mentions that might be faster (as it can read only metadata, not the whole file).

stalkerGH commented 3 weeks ago

Thank you for the hint. Maybe I can try, just for testing purposes, on the site copy. But first I should convert all .webp to .png. I chose this format because of smaller files. I think I can withstand a longer build time - until it reaches an hour or so :)

stalkerGH commented 3 weeks ago

OK, I was so curious about this thing that I:

1) made a backup of whole project, 2) batch converted all .webp to .png, 3) removed all .webp, 4) changed all extensions .webp to .png in .md files, 5) build the page again.

But I got the message:

Error: Failed to serve the site
Error: Failed to render content of /path/to/index.md
Error: Reason: Failed to render image_toggler shortcode
Error: Reason: Failed to render 'shortcodes/image_toggler.html'
Error: Reason: Function call 'get_image_metadata' failed
Error: Reason: `resize_image`: Failed to read image: /path/to/image.png

But the file exists and I can read/display it. pnginfo shows it is valid.

stalkerGH commented 3 weeks ago

Side effect is that content directory swelled from 66 to 507 MB :)

welpo commented 3 weeks ago

I've had issues with seemingly OK images not working well with Zola. I would try changing the problematic png with another one, preferrably one that you haven't converted yourself.

stalkerGH commented 3 weeks ago

I've had issues with seemingly OK images not working well with Zola. I would try changing the problematic png with another one, preferrably one that you haven't converted yourself.

I've done as you recommended. There were three problematic PNGs. I converted them from PNG to PNG ;) and Zola started it's work. Build time is stunning:

Building site... Checking all internal links with anchors. Successfully checked 1 internal link(s) with anchors. -> Creating 12 pages (0 orphan) and 3 sections Done in 226ms.

Maybe I could optimize my PNGs. Will try if it is worth of effort. `

welpo commented 3 weeks ago

Build time is stunning

Yay! 🎉

Maybe I could optimize my PNGs

I've had great (lossless) results with oxipng. pngquant looks promising if you want lossy compression as well.

stalkerGH commented 3 weeks ago

I've had great (lossless) results with oxipng. pngquant looks promising if you want lossy compression as well.

Thank you very much! :)

stalkerGH commented 2 weeks ago

If build time is important, you could try using png instead of webp; the Zola discussion I linked mentions that might be faster (as it can read only metadata, not the whole file).

I reopen the issue. If I understand properly, the problem is in WEBP files. When I use PNG, site is build instantly after change detection. Is there some method or hack to read WEBP metadata as it is done for PNG files?

welpo commented 2 weeks ago

Is there some method or hack to read WEBP metadata as it is done for PNG files?

That would need to be on the Zola side of things. You might want to ask on their issue tracker.

On the tabi side, we could try to modify the image shortcodes. At the cost of potential rendering issues, you could try editing the shortcodes so they don't read image metadata.

For example, a simplified dimmable image shortcode that doens't read metadata (and also doesn't work with relative paths):

{%- set lazy_loading = lazy_loading | default(value=true) -%}

{% if full_width | default(value=false) %}
    <div class="full-width">
{% endif %}
<img class="dimmable-image" src="{{ get_url(path=src) }}"{% if lazy_loading %} loading="lazy"{% endif %}{% if alt %} alt="{{ alt }}"{% endif %}/>
{% if full_width | default(value=false) %}
    </div>
{% endif %}

For the older version of image shortcodes, prior to the relative path feature (which forces metadata reading), see this.

If you edit the shortcode you're using to not use metadata, and it speeds up the webp situation, we might consider what to do next. I'm thinking a ignore_metadata option could be added to shortcodes, for example.

stalkerGH commented 2 weeks ago

Thanks for in-depth explanation. I made some experiments. First, I changed the shortcodes removing this part:

{% if default_meta.width %} width="{{ default_meta.width }}"{% endif %}{% if default_meta.height %} height="{{ default_meta.height }}"{% endif %}

But it changed nothing - at least nothing what I could catch.

Then I downloaded older version of full_width.html and image_toggler.html which I use in my code. This improved things a lot - site builds in miliseconds.

Because both - old and current (latest) - .html files include metadata reading code, in my opinion problem is not related to metadata itself. It has something to do with checking the paths to images. But if you don't have time to investigate it or just don't think it is some crucial thing to solve, it's OK. Maybe this is only my "problem". If this "hack" - reverting to old version of .hmtl files - works, it is sufficient for my needs (as long as it works).

stalkerGH commented 2 weeks ago

It was more complicated that I thought in the very beginning. I didn't realized that after reverting to old versions of .html files images stopped being rendered. So I had to adapt the code to my needs and remove everything which I don't use - for example remote URLs for images.

PS. I'd like to attach my patches but don't know how to do that - I don't know if they are usable but maybe you will be curious to know what I messed up in your code :)

welpo commented 2 weeks ago

It's not clear to me what the culprit is, then. Is it the relative path handling? Did you figure out a way to speed it up WITH metadata reading (and image rendering)?

I'd like to attach my patches but don't know how to do that

You could link to your repo files (if public) or just paste the code here.

stalkerGH commented 2 weeks ago

OK, I will paste whole files (not the patches) in separate messages. I don't know if it has or not something in common with metadata - I suspect rather relative path.

stalkerGH commented 2 weeks ago

full_width_image.html

{%- set colocated_path = page.colocated_path | default(value="") -%}
{%- set relative_path = colocated_path ~ src -%}
{%- set image_url = get_url(path=relative_path) -%}

{%- set lazy_loading = lazy_loading | default(value=true) -%}

<div class="full-width">
    <img src="{{ image_url }}"{% if alt %} alt="{{ alt }}"{% endif %}{% if lazy_loading %} loading="lazy"{% endif %}/>
</div>
stalkerGH commented 2 weeks ago

image_toggler.html

{# The `random_id` ensures that each instance of the shortcode has a "unique" id #}
{# allowing individual interactive elements (like toggles) to function correctly. #}
{# This avoids conflicts when multiple instances of the shortcode are used. #}
{%- set random_id = get_random(end=100000) -%}
{%- set colocated_path = page.colocated_path | default(value="") -%}
{%- set lazy_loading = lazy_loading | default(value=true) -%}
{%- set inline = inline | default(value=false) -%}

{#- Determine the class for the images -#}
{#- Necessary for inline images -#}
{%- set tag = "div" -%}
{%- if inline -%}
    {%- set tag = "span" -%}
{%- endif -%}

{%- set img_class_list = "" -%}
{%- if inline -%}
    {%- set img_class_list = img_class_list ~ " inline" -%}
{%- endif -%}

{%- set relative_default_path = colocated_path ~ default_src -%}
{%- set default_image_url = get_url(path=relative_default_path) -%}

{%- set relative_toggled_path = colocated_path ~ toggled_src -%}
{%- set toggled_image_url = get_url(path=relative_toggled_path) -%}

<{{ tag }} class="image-toggler-container {% if full_width %}full-width{% endif %}">
    <input type="checkbox" id="toggle-img-{{ random_id }}" class="image-toggler-toggle">
    <label for="toggle-img-{{ random_id }}" class="image-label">
        <{{ tag }} class="image-default">
            <img class="{{ img_class_list }}" src="{{ default_image_url }}"{% if lazy_loading %} loading="lazy"{% endif %}{% if default_alt %} alt="{{ default_alt }}"{% endif %}>
        </{{ tag }}>
        <{{ tag }} class="image-toggled">
            <img class="{{ img_class_list }}" src="{{ toggled_image_url }}"{% if lazy_loading %} loading="lazy"{% endif %}{% if toggled_alt %} alt="{{ toggled_alt }}"{% endif %}>
        </{{ tag }}>
    </label>
</{{ tag }}>
welpo commented 2 weeks ago

I don't know if it has or not something in common with metadata - I suspect rather relative path

Neither of the two files reads the metadata, so we can't tell.

Try this for image toggler and this for the full width.

Those versions do read the metadata, but don't try to load relative paths. Note: they require you use absolute paths on the shortcodes, though.

Can you test whether this changes build times?

stalkerGH commented 2 weeks ago

I tested both files now and in the morning - they don't render images. Build seems to go but images are broken in the webpage.

welpo commented 2 weeks ago

The shortcodes did work, so there's most likely a problem with the path. Are you using an absolute path? E.g. blog/your_post/graph.webp

welpo commented 2 weeks ago

If you're hosting the site on a public repo (or wouldn't mind sharing it with me/the world), feel free to drop a link; I could test this stuff myself. I appreciate your time~

stalkerGH commented 2 weeks ago

The shortcodes did work, so there's most likely a problem with the path. Are you using an absolute path? E.g. blog/your_post/graph.webp

No, i use relative path: just image.webp. But when I use the .html files which you pointed me earlier to, with absolute path, the site is built as expected - the images are rendered.

stalkerGH commented 2 weeks ago

If you're hosting the site on a public repo (or wouldn't mind sharing it with me/the world), feel free to drop a link; I could test this stuff myself. I appreciate your time~

Unfortunately I don't use a repo for my website - it is public domain but no source available.

welpo commented 2 weeks ago

But when I use the .html files which you pointed me earlier to, with absolute path, the site is built as expected - the images are rendered.

And how's the speed with webp files?

stalkerGH commented 2 weeks ago

And how's the speed with webp files?

Very fast. 15 pages, about 150 images (including several PNGs) - build time is 250-370 ms.

welpo commented 2 weeks ago

And if you switch from the old shortcodes to the current version, without changing the png/webp files or paths, it slows down?

stalkerGH commented 2 weeks ago

If you mean current as latest in repo then answer is - yes, it is much slower, about 15 seconds.

welpo commented 2 weeks ago

Just to triple check: it's slow even when using absolute paths? (with current shortcodes)

welpo commented 2 weeks ago

Actually, I'm still confused: current shortcodes were fast with PNG files, right? Have we isolated all variables?

stalkerGH commented 2 weeks ago

Actually, I'm still confused: current shortcodes were fast with PNG files, right? Have we isolated all variables?

Yes, build time was much faster with PNGs, with latest versions of .html shortcodes.

stalkerGH commented 2 weeks ago

Just to triple check: it's slow even when using absolute paths? (with current shortcodes)

Yes, I just checked it out. I disabled most of files with draft = true and build time was 2.4 seconds. Then I enabled one file with ~40 WEBP images, added absolute paths to all images and build time is 3 times longer - 7.6 seconds.

welpo commented 2 weeks ago

Thank you for all this testing.

Can you do one more? Let's try this full width:

{%- set meta = get_image_metadata(path=src, allow_missing=true) -%}
{%- set image_url = get_url(path=src) -%}

{%- set lazy_loading = lazy_loading | default(value=true) -%}

<div class="full-width">
    <img src="{{ image_url }}"{% if alt %} alt="{{ alt }}"{% endif %}{% if meta.width %} width="{{ meta.width }}"{% endif %}{% if meta.height %} height="{{ meta.height }}"{% endif %}{% if lazy_loading %} loading="lazy"{% endif %}/>
</div>

This removes the relative path logic. My hypothesis:

stalkerGH commented 2 weeks ago

I used above code, disabled all pages (draft = true) and build time was ~250 ms. Then I enabled one page with many images and build time was ~2.5 seconds. All with WEBP images.

stalkerGH commented 2 weeks ago

When I converted all WEBPs to PNGs in a directory which belongs to one and only file which was enabled to build, build time is ~250 ms. So something is "broken" with WEBPs.

EDIT: no so fast... No images are rendered. Just for case I made this operation: oxipng -o 4 --strip safe --alpha *.png - no render at all. Coming back to WEBP - rendering as expected.

EDIT no. 2: images not render if path is relative but rendering is OK whet path is absolute.

welpo commented 2 weeks ago

No images are rendered

Did you fix this? I can render PNGs with the shortcode I posted.

images not render if path is relative but rendering is OK whet path is absolute.

Ah, yes, that's expected.


So, to clarify: did you try rendering many PNGs?

stalkerGH commented 2 weeks ago

I converted WEBPs to PNGs only in the directory where the site enabled for rendering is seated. All other pages are disabled by draft = true so I didn't converted these WEBPs to PNGs. Is that what you are asking for?

welpo commented 2 weeks ago

Yes, that's fine. I'm trying to verify the hypotheses:

  • It will be slow with webp (slower than this)

    • It will be fast with png

We confirmed it's slow with webp, and I'm 99.9% sure it's fast with PNG, but wanted to confirm.

stalkerGH commented 2 weeks ago

Can I do something more yet?

welpo commented 2 weeks ago

Thanks again for helping with this. Let's try this:

If you are using absolute paths:

{%- set read_metadata = read_metadata | default(value=true) -%}

{#- Set paths based on whether the src is remote or local -#}
{%- if src is starting_with("http") -%}
    {%- set image_url = src -%}
{%- elif read_metadata -%}
    {%- set colocated_path = page.colocated_path | default(value="") -%}
    {%- set relative_path = colocated_path ~ src -%}
    {%- set meta = get_image_metadata(path=relative_path, allow_missing=true) -%}

    {#- Fallback to absolute path if relative path doesn't work -#}
    {%- if not meta -%}
        {%- set meta = get_image_metadata(path=src, allow_missing=true) -%}
        {%- set image_url = get_url(path=src) -%}
    {%- else %}
        {%- set image_url = get_url(path=relative_path) -%}
    {%- endif -%}
{%- else -%}
    {%- set image_url = get_url(path=src) -%}
{%- endif -%}

{%- set lazy_loading = lazy_loading | default(value=true) -%}

<div class="full-width">
    <img src="{{ image_url }}"{% if alt %} alt="{{ alt }}"{% endif %}{% if read_metadata and meta.width %} width="{{ meta.width }}"{% endif %}{% if read_metadata and meta.height %} height="{{ meta.height }}"{% endif %}{% if lazy_loading %} loading="lazy"{% endif %}/>
</div>

If you want to test with relative paths:

{%- set read_metadata = read_metadata | default(value=true) -%}

{#- Set paths based on whether the src is remote or local -#}
{%- if src is starting_with("http") -%}
    {%- set image_url = src -%}
{%- else -%}
    {%- set colocated_path = page.colocated_path | default(value="") -%}
    {%- set relative_path = colocated_path ~ src -%}

    {%- if read_metadata -%}
        {%- set meta = get_image_metadata(path=relative_path, allow_missing=true) -%}

        {#- Fallback to absolute path if relative path doesn't work -#}
        {%- if not meta -%}
            {%- set meta = get_image_metadata(path=src, allow_missing=true) -%}
            {%- set image_url = get_url(path=src) -%}
        {%- else %}
            {%- set image_url = get_url(path=relative_path) -%}
        {%- endif -%}
    {%- else -%}
        {%- set image_url = get_url(path=relative_path) -%}
    {%- endif -%}
{%- endif -%}

{%- set lazy_loading = lazy_loading | default(value=true) -%}

<div class="full-width">
    <img src="{{ image_url }}"{% if alt %} alt="{{ alt }}"{% endif %}{% if read_metadata and meta.width %} width="{{ meta.width }}"{% endif %}{% if read_metadata and meta.height %} height="{{ meta.height }}"{% endif %}{% if lazy_loading %} loading="lazy"{% endif %}/>
</div>

Both shortcode do the same, except one ONLY works with full paths, and the other with relative paths. Use whichever requires less work for you.

This is the current shortcode for full width with an extra option: read_metadata. If set to false, it will skip reading the metadata.

If possible, can you test this with webp?

  1. Change the code to your shortcode
  2. Check how much time it takes to build with render_metadata=false (should be super fast)
  3. Check how much time it takes to build with render_metadata=true (should be slow)

Make sure images render in both cases. The shortcode should work with both absolute and relative paths.

An example of calling the shortcode (first version, with full paths) with this option:

{{ full_width_image(src="blog/shortcodes/img/amsterdam_by_oskerwyld.png", read_metadata=false) }}
stalkerGH commented 2 weeks ago

Thank you for the effort. If you allow, I will conduct tests tomorrow. Sometimes you need to sleep :)