← home

Image Optimization Tools

Image Optimization Tools

Overview

This directory contains tools for optimizing images on bankole.org with a focus on performance and GitHub Pages compatibility.

optimize-images.sh

Automated image optimization script that generates WebP versions and responsive image sizes.

Features

Usage

# Optimize a single image
bash tools/optimize-images.sh path/to/image.jpg

# Optimize all images in a directory
bash tools/optimize-images.sh assets/images/posts/

# Preview what would be optimized (dry run)
bash tools/optimize-images.sh --dry-run image.jpg

# Custom quality settings
bash tools/optimize-images.sh --quality 85 --webp-quality 80 image.jpg

Configuration

Default settings in the script:

Output

For each input image, the script generates:

  1. Original optimized JPEG (if over 500KB or wider than 1920px)
    • Example: image.jpg
  2. WebP version
    • Example: image.webp
  3. Responsive JPEG versions
    • Example: image-640w.jpg, image-1280w.jpg, image-1920w.jpg
  4. Responsive WebP versions
    • Example: image-640w.webp, image-1280w.webp, image-1920w.webp

Requirements

Example

# Before
boracay-beach.jpeg: 1.5MB (4032x3024)

# After running optimization
boracay-beach.jpg: 933KB (1920x1440)
boracay-beach.webp: 397KB
boracay-beach-1920w.jpg: 933KB
boracay-beach-1920w.webp: 397KB
boracay-beach-1280w.jpg: 431KB
boracay-beach-1280w.webp: 184KB
boracay-beach-640w.jpg: 108KB
boracay-beach-640w.webp: 47KB

Performance Impact

Integration with Jekyll

The optimized images work with the enhanced _includes/lazy-image.html component:

















<div class="lazy-image-wrapper">
  <picture>
    
    <source
      type="image/webp"
      data-srcset="/assets/images/posts/photo-640w.webp 640w,
                   /assets/images/posts/photo-1280w.webp 1280w,
                   /assets/images/posts/photo-1920w.webp 1920w"
      sizes="(max-width: 640px) 640px, (max-width: 1280px) 1280px, 1920px"
    />

    
    <source
      type="image/jpeg"
      data-srcset="/assets/images/posts/photo-640w.jpg 640w,
                   /assets/images/posts/photo-1280w.jpg 1280w,
                   /assets/images/posts/photo-1920w.jpg 1920w"
      sizes="(max-width: 640px) 640px, (max-width: 1280px) 1280px, 1920px"
    />

    
    <img
      data-src="/assets/images/posts/photo.jpg"
      alt="Description"
      class="lazy-image "
      loading="lazy"
      width="1920"
      height="1440"
    />
  </picture>

  <noscript>
    <picture>
      <source
        type="image/webp"
        srcset="/assets/images/posts/photo-640w.webp 640w,
                /assets/images/posts/photo-1280w.webp 1280w,
                /assets/images/posts/photo-1920w.webp 1920w"
        sizes="(max-width: 640px) 640px, (max-width: 1280px) 1280px, 1920px"
      />
      <img
        src="/assets/images/posts/photo.jpg"
        srcset="/assets/images/posts/photo-640w.jpg 640w,
                /assets/images/posts/photo-1280w.jpg 1280w,
                /assets/images/posts/photo-1920w.jpg 1920w"
        sizes="(max-width: 640px) 640px, (max-width: 1280px) 1280px, 1920px"
        alt="Description"
        class=""
        width="1920"
        height="1440"
      />
    </picture>
  </noscript>
</div>

<script>
(function() {
  if (!window.lazyImageObserver) {
    window.lazyImageObserver = {
      init: function() {
        var images = document.querySelectorAll('img.lazy-image[data-src]');
        var sources = document.querySelectorAll('source[data-srcset]');

        // Function to load an image and its sources
        var loadImage = function(img) {
          // Load source elements first
          var picture = img.closest('picture');
          if (picture) {
            var pictureSources = picture.querySelectorAll('source[data-srcset]');
            pictureSources.forEach(function(source) {
              if (source.dataset.srcset) {
                source.srcset = source.dataset.srcset;
                source.removeAttribute('data-srcset');
              }
            });
          }

          // Load img element
          if (img.dataset.src) {
            img.src = img.dataset.src;
            img.removeAttribute('data-src');
            img.classList.add('lazy-loaded');

            if (window.performance && window.performance.mark) {
              performance.mark('image-loaded');
            }
          }
        };

        if ('loading' in HTMLImageElement.prototype) {
          // Native lazy loading support
          images.forEach(function(img) {
            loadImage(img);
          });
        } else if ('IntersectionObserver' in window) {
          // IntersectionObserver fallback
          var imageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
              if (entry.isIntersecting) {
                var img = entry.target;
                loadImage(img);
                observer.unobserve(img);
              }
            });
          }, {
            rootMargin: '50px'
          });

          images.forEach(function(img) {
            imageObserver.observe(img);
          });
        } else {
          // Fallback for older browsers - load everything
          images.forEach(function(img) {
            loadImage(img);
          });
        }
      }
    };

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', window.lazyImageObserver.init);
    } else {
      window.lazyImageObserver.init();
    }
  }
})();
</script>

The component automatically:

  1. Serves WebP to supporting browsers
  2. Uses responsive srcset for different viewport sizes
  3. Lazy-loads images as users scroll
  4. Prevents layout shift with width/height attributes

Testing

Run the image optimization test suite:

bash tests/image-optimization-tests.sh

Tests verify:

Best Practices

  1. Always optimize before committing
    bash tools/optimize-images.sh assets/images/posts/new-image.jpg
    git add assets/images/posts/
    
  2. Use descriptive alt text in the lazy-image component

  3. Specify width and height to prevent layout shift

  4. Test on multiple devices to verify responsive images load correctly

  5. Check file sizes after optimization:
    ls -lh assets/images/posts/ | grep "new-image"
    

Troubleshooting

WebP files not generated

Images too large

Responsive versions missing

Performance Metrics

Metric Before After Improvement
Largest image 1.7MB 424KB (WebP) 75% smaller
Average image size 1.6MB 436KB 73% smaller
Total payload (3 images) 4.8MB 1.3MB 73% reduction
Mobile viewport (640w) 4.8MB 156KB 97% reduction

Future Enhancements