Overview
Image optimization refers to the process of reducing image file sizes while maintaining acceptable visual quality. Images typically account for 50-70% of total page weight in web applications, making optimization critical for performance. The practice encompasses format selection, compression techniques, dimension adjustment, and delivery strategies.
The core challenge stems from balancing three factors: visual quality, file size, and processing time. Different image types require different approaches. Photographs benefit from lossy compression formats like JPEG, while graphics with sharp edges require lossless formats like PNG. Modern formats like WebP and AVIF provide superior compression ratios but require fallback strategies for older browsers.
Image optimization occurs at multiple stages in the development pipeline. Server-side processing handles uploads and generates variants. Build-time optimization processes static assets. Runtime strategies include lazy loading, responsive image selection, and progressive loading. Each stage presents opportunities for size reduction and performance gains.
# Basic image optimization flow
class ImageProcessor
def optimize(source_path)
image = load_image(source_path)
image = resize_if_needed(image)
image = compress(image)
save_optimized(image)
end
end
The impact of optimization extends beyond bandwidth savings. Faster image loading improves perceived performance, reduces bounce rates, and affects search engine rankings. Mobile users on limited connections particularly benefit from optimized images. A 1MB image taking 10 seconds to load on 3G can be reduced to 100KB loading in under 1 second through proper optimization.
Key Principles
Image optimization operates on several fundamental principles that govern how file sizes can be reduced without unacceptable quality loss.
Format Selection: Different formats serve different purposes. JPEG uses lossy compression suitable for photographs with gradual color transitions. PNG uses lossless compression appropriate for graphics with sharp edges and transparency. WebP provides both lossy and lossless modes with better compression than JPEG/PNG. AVIF offers the best compression ratios but limited browser support. SVG stores vector graphics as XML, remaining sharp at any scale.
Compression Methods: Lossy compression discards data that human vision handles poorly, achieving higher compression ratios. JPEG quality settings typically range from 0-100, with 85-90 providing good balance. Lossless compression reorganizes data without information loss through techniques like run-length encoding and Huffman coding. PNG optimization tools recompress without quality change.
Dimension Management: Serving images larger than display dimensions wastes bandwidth. An image displayed at 400px width should not exceed 800px actual width (accounting for high-DPI displays). Responsive images use the picture element or srcset attribute to serve appropriate sizes based on viewport and display density.
Color Depth Reduction: Images often contain more colors than necessary. Reducing bit depth from 24-bit (16.7 million colors) to 8-bit (256 colors) can dramatically reduce file size for graphics and illustrations. Quantization algorithms intelligently select the most representative colors.
Progressive Loading: Progressive JPEGs load in increasing quality passes rather than top-to-bottom. Users see a low-quality preview quickly, then refinements load. This improves perceived performance compared to baseline JPEGs that render line-by-line.
# Compression quality comparison
class CompressionAnalyzer
def analyze(image_path)
original_size = File.size(image_path)
[100, 90, 80, 70, 60].each do |quality|
compressed = compress_jpeg(image_path, quality)
compressed_size = File.size(compressed)
ratio = (compressed_size.to_f / original_size * 100).round(1)
puts "Quality #{quality}: #{compressed_size} bytes (#{ratio}%)"
end
end
end
Metadata Removal: Images contain metadata like EXIF data (camera settings, GPS coordinates, timestamps), color profiles, and thumbnails. This metadata often serves no purpose in web delivery and can be stripped to reduce file size by 10-30KB per image.
Chroma Subsampling: JPEG compression can subsample color information more aggressively than brightness information, exploiting how human vision is more sensitive to luminance than chrominance. 4:2:0 subsampling reduces color resolution by 75% with minimal perceptible quality loss.
Ruby Implementation
Ruby provides multiple libraries for image processing and optimization, each with different performance characteristics and dependencies.
MiniMagick: A lightweight wrapper around ImageMagick command-line tools. It processes images by shelling out to ImageMagick binaries, making it more memory-efficient than loading entire image processing libraries.
require 'mini_magick'
class ImageOptimizer
def optimize_jpeg(input_path, output_path, quality: 85)
image = MiniMagick::Image.open(input_path)
image.format 'jpg'
image.quality quality
image.strip # Remove metadata
image.interlace 'Plane' # Progressive JPEG
image.write output_path
end
def optimize_png(input_path, output_path)
image = MiniMagick::Image.open(input_path)
image.format 'png'
image.strip
image.define 'png:compression-level=9'
image.write output_path
end
end
ImageProcessing: A higher-level gem that provides a unified interface supporting both ImageMagick (via MiniMagick) and libvips. It offers chainable operations and automatic processor selection.
require 'image_processing/mini_magick'
class ResponsiveImageGenerator
def generate_variants(source_path)
pipeline = ImageProcessing::MiniMagick
.source(source_path)
.strip
.saver(quality: 85, interlace: 'Plane')
{
small: pipeline.resize_to_limit(400, 400),
medium: pipeline.resize_to_limit(800, 800),
large: pipeline.resize_to_limit(1200, 1200)
}.transform_values { |p| p.call }
end
end
Vips: Ruby bindings for libvips, a fast image processing library. Libvips processes images in streaming fashion, using significantly less memory than ImageMagick. For large images or bulk processing, vips offers 10-20x faster performance.
require 'vips'
class VipsOptimizer
def optimize(input_path, output_path)
image = Vips::Image.new_from_file(input_path)
# Resize if too large
if image.width > 2000
scale = 2000.0 / image.width
image = image.resize(scale)
end
# Save with optimization
image.write_to_file(output_path,
Q: 85, # Quality
strip: true, # Remove metadata
optimize_coding: true,
interlace: true # Progressive
)
end
end
ImageOptim: A gem that wraps multiple optimization tools (pngcrush, optipng, jpegoptim, etc.) and runs them in sequence to achieve maximum compression. It requires installing the underlying tools but produces the best results.
require 'image_optim'
class BatchOptimizer
def initialize
@optimizer = ImageOptim.new(
pngout: false, # Slow, disable for faster processing
svgo: true,
jpegoptim: { max_quality: 85 }
)
end
def optimize_directory(path)
Dir.glob("#{path}/**/*.{jpg,png,gif,svg}") do |file|
result = @optimizer.optimize_image(file)
if result
original_size = File.size(file)
optimized_size = File.size(result.path)
saved = original_size - optimized_size
percent = (saved.to_f / original_size * 100).round(1)
puts "#{file}: saved #{saved} bytes (#{percent}%)"
end
end
end
end
Active Storage Integration: Rails Active Storage integrates with image processing gems to generate variants on-demand. Variants are cached and served from storage.
class Product < ApplicationRecord
has_one_attached :image
def optimized_image_url(variant_name)
variants = {
thumbnail: image.variant(resize_to_limit: [150, 150],
saver: { quality: 80, strip: true }),
display: image.variant(resize_to_limit: [800, 800],
saver: { quality: 85, strip: true, interlace: 'Plane' }),
large: image.variant(resize_to_limit: [1600, 1600],
saver: { quality: 90, strip: true, interlace: 'Plane' })
}
variants[variant_name].processed.url
end
end
Carrierwave Integration: Carrierwave provides uploaders with processing callbacks. Optimization occurs during upload processing.
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
process :strip_metadata
process :optimize_format
version :thumbnail do
process resize_to_fill: [200, 200]
process quality: 80
end
version :display do
process resize_to_limit: [800, 800]
process quality: 85
end
def strip_metadata
manipulate! do |img|
img.strip
img
end
end
def optimize_format
manipulate! do |img|
img.format('jpg') if img.mime_type == 'image/png' && img.size > 500_000
img.interlace('Plane')
img
end
end
end
Performance Considerations
Image optimization directly affects application performance across multiple metrics: page load time, bandwidth consumption, server CPU usage, and user experience.
Bandwidth Savings: A typical unoptimized photograph might be 2-3MB. Proper optimization reduces this to 200-400KB without visible quality loss—an 85-90% reduction. For sites serving millions of images, this translates to significant bandwidth cost savings and faster load times for users.
Processing Overhead: Image optimization requires CPU time. On-the-fly optimization during upload can introduce latency. Processing a 5MB image with ImageMagick might take 2-5 seconds, while libvips completes the same task in 200-500ms. Background job processing prevents blocking user requests.
class ImageUploadJob < ApplicationJob
queue_as :default
def perform(image_id)
image = Image.find(image_id)
source = download_original(image.url)
# Generate multiple optimized variants
variants = {}
benchmark = Benchmark.measure do
variants = {
thumbnail: optimize(source, max_width: 200, quality: 75),
medium: optimize(source, max_width: 800, quality: 85),
large: optimize(source, max_width: 1600, quality: 90)
}
end
# Store processing time for monitoring
image.update(
variants: variants,
processing_time: benchmark.real
)
end
end
Memory Usage: ImageMagick loads entire images into memory, which becomes problematic for large images or concurrent processing. A 4000x3000 pixel RGB image consumes ~36MB of RAM. Libvips uses streaming processing, keeping memory usage constant regardless of image size—typically 50-100MB total.
Caching Strategy: Generating variants on every request wastes resources. Proper caching stores processed variants and serves them directly. CDN caching further reduces origin server load.
class ImageVariantCache
def fetch(image_id, variant_name)
cache_key = "image:#{image_id}:#{variant_name}"
Rails.cache.fetch(cache_key, expires_in: 30.days) do
image = Image.find(image_id)
generate_variant(image, variant_name)
end
end
def invalidate(image_id)
[:thumbnail, :medium, :large].each do |variant|
Rails.cache.delete("image:#{image_id}:#{variant}")
end
end
end
Lazy Loading: Deferring image loading until images enter the viewport prevents downloading off-screen images. This reduces initial page weight and speeds up perceived load time. Modern browsers support native lazy loading.
module ImageHelper
def optimized_image_tag(image, variant: :medium, lazy: true)
url = image.variant_url(variant)
img_tag = content_tag(:img,
src: lazy ? placeholder_url : url,
'data-src': lazy ? url : nil,
loading: lazy ? 'lazy' : 'eager',
class: lazy ? 'lazyload' : nil
)
end
end
Format Negotiation: Serving modern formats to supporting browsers while falling back to legacy formats maximizes compression benefits. WebP typically saves 25-35% compared to JPEG at equivalent quality. AVIF saves 50% compared to JPEG but has limited support.
class ImageFormatSelector
FORMATS = {
'image/avif' => 'avif',
'image/webp' => 'webp',
'image/jpeg' => 'jpg'
}
def select_format(request)
accept_header = request.headers['Accept']
FORMATS.each do |mime_type, extension|
return extension if accept_header.include?(mime_type)
end
'jpg' # Fallback
end
end
Parallel Processing: Processing multiple images concurrently reduces total processing time. Thread pools prevent overwhelming system resources.
require 'concurrent'
class BulkImageProcessor
def process_batch(image_ids)
pool = Concurrent::FixedThreadPool.new(4)
promises = image_ids.map do |id|
Concurrent::Promise.execute(executor: pool) do
process_single_image(id)
end
end
results = promises.map(&:value)
pool.shutdown
pool.wait_for_termination
results
end
end
Progressive Enhancement: Loading low-quality image previews quickly, then replacing with high-quality versions improves perceived performance. Users see content immediately rather than waiting for full-resolution images.
Tools & Ecosystem
The image optimization ecosystem includes command-line tools, Ruby gems, cloud services, and CDN features.
Command-Line Tools: These tools provide the foundation for optimization. Many Ruby gems wrap these utilities.
| Tool | Format | Type | Strengths |
|---|---|---|---|
| jpegoptim | JPEG | Lossless | Fast, preserves quality |
| jpegtran | JPEG | Lossless | Progressive JPEG support |
| mozjpeg | JPEG | Lossy | Superior compression |
| optipng | PNG | Lossless | Multiple optimization levels |
| pngquant | PNG | Lossy | Color quantization |
| pngcrush | PNG | Lossless | Reduces file size |
| gifsicle | GIF | Both | Animation optimization |
| svgo | SVG | Lossless | Removes unnecessary data |
| cwebp | WebP | Both | WebP encoding |
| avifenc | AVIF | Both | AVIF encoding |
Ruby Gems: Libraries providing high-level interfaces to image processing.
# Gemfile dependencies
gem 'mini_magick' # ImageMagick wrapper
gem 'ruby-vips' # libvips bindings
gem 'image_processing' # Unified interface
gem 'image_optim' # Multi-tool optimizer
gem 'image_optim_pack' # Pre-compiled binaries
gem 'carrierwave' # Upload handling
gem 'shrine' # Upload handling
ImageMagick vs Libvips: The two primary processing libraries have different characteristics.
| Aspect | ImageMagick | Libvips |
|---|---|---|
| Memory Usage | High (full image in RAM) | Low (streaming) |
| Speed | Moderate | Fast (10-20x) |
| Features | Extensive | Focused |
| Installation | Simple | Requires libvips |
| Ruby Gem | mini_magick | ruby-vips |
Cloud Services: Managed services handle optimization automatically.
Cloudinary provides API-based image management with automatic optimization, format selection, and responsive image generation.
require 'cloudinary'
Cloudinary::Uploader.upload('path/to/image.jpg',
transformation: [
{ quality: 'auto', fetch_format: 'auto' },
{ width: 800, crop: 'limit' }
]
)
# Generated URL includes optimizations
url = Cloudinary::Utils.cloudinary_url('image_id',
quality: 'auto:good',
fetch_format: 'auto',
width: 800,
crop: 'limit'
)
Imgix offers real-time image processing via URL parameters.
class ImgixHelper
def imgix_url(image_path, width:, quality: 85)
base_url = 'https://your-source.imgix.net'
params = {
w: width,
q: quality,
auto: 'format,compress'
}
"#{base_url}/#{image_path}?#{URI.encode_www_form(params)}"
end
end
CDN Integration: Content Delivery Networks cache and serve optimized images from edge locations.
Fastly and Cloudflare offer image optimization features through their CDNs. Images are automatically compressed and converted to optimal formats based on browser support.
# Cloudflare Image Resizing via Workers
class CloudflareImageProxy
def self.resize_url(image_url, width:, quality: 85)
params = {
width: width,
quality: quality,
format: 'auto'
}
"/cdn-cgi/image/#{params.map { |k,v| "#{k}=#{v}" }.join(',')}/#{image_url}"
end
end
Monitoring Tools: Tracking optimization effectiveness requires measurement.
class ImageMetrics
def track_optimization(image)
{
original_size: image.original_size,
optimized_size: image.optimized_size,
compression_ratio: calculate_ratio(image),
format: image.format,
dimensions: "#{image.width}x#{image.height}",
processing_time: image.processing_time
}
end
def aggregate_stats
{
total_saved: Image.sum('original_size - optimized_size'),
average_ratio: Image.average('optimized_size::float / original_size'),
total_images: Image.count
}
end
end
Implementation Approaches
Different optimization strategies suit different application architectures and requirements.
Upload-Time Processing: Images are optimized when uploaded. This approach front-loads processing cost but ensures all images are optimized before serving.
class ImageUploadHandler
def handle_upload(file)
# Validate file
return { error: 'Invalid file' } unless valid_image?(file)
# Store original
original_path = store_original(file)
# Generate optimized variants
variants = {
thumbnail: process(original_path, width: 200, quality: 75),
medium: process(original_path, width: 800, quality: 85),
large: process(original_path, width: 1600, quality: 90)
}
# Store metadata
create_image_record(original_path, variants)
end
end
On-Demand Processing: Variants generate when first requested, then cache for subsequent requests. This approach defers processing cost but may introduce latency on first access.
class OnDemandImageProcessor
def serve_image(image_id, variant_name)
cache_path = cached_variant_path(image_id, variant_name)
if File.exist?(cache_path)
return send_file(cache_path)
end
# Generate variant
image = Image.find(image_id)
variant = generate_variant(image.source_path, variant_name)
# Cache for future requests
File.write(cache_path, variant)
send_file(cache_path)
end
end
Background Job Processing: Optimization occurs asynchronously after upload. Users receive immediate feedback while processing happens in background.
class AsyncImageProcessor
def upload(file)
# Store original immediately
image = Image.create(
source: file,
status: 'processing'
)
# Queue optimization job
OptimizeImageJob.perform_later(image.id)
# Return immediately
{ id: image.id, status: 'processing' }
end
end
class OptimizeImageJob < ApplicationJob
def perform(image_id)
image = Image.find(image_id)
variants = generate_all_variants(image.source_path)
image.update(
variants: variants,
status: 'ready',
optimized_at: Time.current
)
rescue => e
image.update(status: 'failed', error: e.message)
end
end
Build-Time Optimization: Static site generators optimize images during build. This approach eliminates runtime processing but requires rebuilding for changes.
# Rakefile or build script
task :optimize_images do
optimizer = ImageOptim.new
Dir.glob('app/assets/images/**/*.{jpg,png}').each do |path|
puts "Optimizing #{path}"
optimizer.optimize_image!(path)
end
end
# Integrate with asset pipeline
task 'assets:precompile' => :optimize_images
Hybrid Approach: Combines multiple strategies. Common variants generate at upload time, while specialized variants generate on-demand.
class HybridImageProcessor
EAGER_VARIANTS = [:thumbnail, :medium]
LAZY_VARIANTS = [:large, :xlarge, :retina]
def process_upload(file)
image = Image.create(source: file)
# Generate common variants immediately
eager_variants = EAGER_VARIANTS.map do |variant_name|
[variant_name, generate_variant(image.source_path, variant_name)]
end.to_h
image.update(variants: eager_variants)
# Queue less common variants for background processing
GenerateLazyVariantsJob.perform_later(image.id)
image
end
end
Practical Examples
Real-world scenarios demonstrate optimization techniques in context.
E-commerce Product Images: Product pages display images at multiple sizes. Optimization reduces bandwidth while maintaining visual quality critical for sales.
class ProductImageOptimizer
def optimize_product_image(upload)
# Store original high-quality version
original = store_original(upload)
# Generate variants for different contexts
variants = {
# Thumbnail for listings (150x150)
thumbnail: generate_variant(original,
width: 150,
height: 150,
quality: 75,
crop: 'fill'
),
# Main product display (800x800)
display: generate_variant(original,
width: 800,
height: 800,
quality: 85,
crop: 'fit',
progressive: true
),
# Zoom/lightbox (1600x1600)
zoom: generate_variant(original,
width: 1600,
height: 1600,
quality: 90,
crop: 'fit',
progressive: true
),
# Retina thumbnail (300x300)
thumbnail_2x: generate_variant(original,
width: 300,
height: 300,
quality: 75,
crop: 'fill'
)
}
# Generate WebP versions for supported browsers
webp_variants = variants.transform_values do |path|
convert_to_webp(path, quality: 80)
end
{
original: original,
variants: variants,
webp_variants: webp_variants
}
end
end
User Avatar Processing: User-uploaded avatars require consistent sizing and format. Face detection ensures important content remains visible after cropping.
require 'ruby-vips'
class AvatarProcessor
SIZES = {
small: 48,
medium: 96,
large: 192
}
def process_avatar(upload_path)
image = Vips::Image.new_from_file(upload_path)
# Detect faces for smart cropping
faces = detect_faces(image)
# Generate sized variants
variants = SIZES.map do |size_name, dimension|
variant = if faces.any?
smart_crop_to_face(image, faces.first, dimension)
else
center_crop(image, dimension)
end
# Optimize
optimized = variant
.thumbnail_image(dimension, height: dimension, crop: :centre)
.jpegsave_buffer(Q: 85, strip: true, optimize_coding: true)
[size_name, save_variant(optimized, size_name)]
end.to_h
variants
end
private
def smart_crop_to_face(image, face_coords, target_size)
# Calculate crop region centered on face
x, y, width, height = face_coords
center_x = x + width / 2
center_y = y + height / 2
crop_size = [width, height].max * 1.5
left = [0, center_x - crop_size / 2].max
top = [0, center_y - crop_size / 2].max
image.crop(left, top, crop_size, crop_size)
end
end
Responsive Image Generation: Serving appropriately-sized images based on viewport requires multiple variants and proper HTML markup.
class ResponsiveImageGenerator
BREAKPOINTS = {
mobile: 480,
tablet: 768,
desktop: 1200,
large: 1920
}
def generate_responsive_set(source_path)
# Generate image at each breakpoint
variants = BREAKPOINTS.map do |name, width|
variant_path = optimize_for_width(source_path, width)
[name, variant_path]
end.to_h
# Generate WebP versions
webp_variants = variants.transform_values do |path|
convert_to_webp(path)
end
{
variants: variants,
webp_variants: webp_variants,
srcset: build_srcset(variants),
srcset_webp: build_srcset(webp_variants)
}
end
def build_srcset(variants)
BREAKPOINTS.map do |name, width|
"#{variants[name]} #{width}w"
end.join(', ')
end
def build_picture_tag(image_data, alt_text)
<<~HTML
<picture>
<source
type="image/webp"
srcset="#{image_data[:srcset_webp]}"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw">
<source
type="image/jpeg"
srcset="#{image_data[:srcset]}"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw">
<img
src="#{image_data[:variants][:desktop]}"
alt="#{alt_text}"
loading="lazy">
</picture>
HTML
end
end
Blog Post Image Processing: Blog images require optimization balanced with editorial quality. Images include captions, alt text, and srcset attributes.
class BlogImageProcessor
def process_blog_image(upload, metadata = {})
# Maintain higher quality for editorial content
variants = {
content: optimize_for_content(upload,
max_width: 1200,
quality: 90
),
preview: optimize_for_preview(upload,
max_width: 600,
quality: 85
)
}
# Generate responsive sizes
responsive_variants = [400, 600, 800, 1000, 1200].map do |width|
["width_#{width}", resize_to_width(upload, width, quality: 88)]
end.to_h
# Create metadata record
{
variants: variants.merge(responsive_variants),
alt_text: metadata[:alt_text],
caption: metadata[:caption],
credit: metadata[:credit],
srcset: build_srcset_with_sizes(responsive_variants)
}
end
def build_srcset_with_sizes(variants)
variants.map do |key, path|
width = key.to_s.split('_').last
"#{path} #{width}w"
end.join(', ')
end
end
Common Pitfalls
Image optimization involves numerous potential mistakes that reduce quality or increase file sizes unexpectedly.
Recompressing Already-Compressed Images: Applying lossy compression multiple times compounds quality loss. Each recompression generation loses additional data. Always optimize from the original source, not previously compressed versions.
# Wrong: Recompressing existing JPEG
def broken_optimization(jpeg_path)
image = MiniMagick::Image.open(jpeg_path) # Already compressed
image.quality 85 # Recompresses, loses more quality
image.write(jpeg_path)
end
# Correct: Keep original, generate variants
def correct_optimization(original_path)
Image.create(
original: store_uncompressed(original_path),
variants: generate_optimized_variants(original_path)
)
end
Ignoring Display Dimensions: Serving full-resolution images when smaller versions suffice wastes bandwidth. A 4000x3000 image displayed at 400x300 delivers unnecessary data.
# Wrong: Always serving full size
def serve_image(image_id)
image = Image.find(image_id)
send_file(image.original_path) # Could be 5MB
end
# Correct: Serve appropriate variant
def serve_image(image_id, display_width)
image = Image.find(image_id)
variant = select_variant_for_width(image, display_width)
send_file(variant.path) # Much smaller
end
Format Mismatch: Using PNG for photographs or JPEG for graphics with transparency produces inefficient files. PNG photographs can be 10x larger than equivalent JPEG. JPEG cannot handle transparency, requiring workarounds.
Excessive Quality Settings: JPEG quality above 90-95 produces diminishing returns. File size increases dramatically with minimal perceptible improvement. Quality 100 disables compression entirely.
Stripping Needed Metadata: Removing all EXIF data indiscriminately can delete important information like copyright, orientation, or color profiles. Orientation tags prevent images displaying sideways. Color profiles ensure accurate color representation.
# Wrong: Strip everything
def over_aggressive_stripping(image_path)
image = MiniMagick::Image.open(image_path)
image.strip # Removes ALL metadata including orientation
image.write(image_path)
end
# Correct: Preserve essential metadata
def selective_stripping(image_path)
image = MiniMagick::Image.open(image_path)
image.auto_orient # Fix orientation first
image.strip # Now safe to remove metadata
image.write(image_path)
end
Forgetting High-DPI Displays: Serving 1x images to Retina displays produces blurry results. High-DPI displays require 2x or 3x resolution images, though these can use lower quality settings to compensate for file size.
Blocking Uploads: Processing large images synchronously blocks web requests, creating poor user experience. A 10MB upload might take 10-20 seconds to process, timing out connections.
# Wrong: Synchronous processing in controller
def create
uploaded_file = params[:image]
optimized = optimize_image(uploaded_file) # Blocks for 10+ seconds
render json: { url: optimized.url }
end
# Correct: Async processing
def create
image = Image.create(source: params[:image], status: 'processing')
OptimizeImageJob.perform_later(image.id)
render json: { id: image.id, status: 'processing' }
end
Ignoring Browser Support: Serving WebP or AVIF to browsers that do not support them breaks images. Format negotiation or picture element fallbacks prevent this issue.
Subsampling Artifacts: Aggressive chroma subsampling creates visible artifacts in images with sharp red/blue edges or small text. 4:2:0 subsampling works for photographs but damages graphics.
Reference
Compression Quality Guidelines
| Image Type | Format | Quality Setting | Use Case |
|---|---|---|---|
| Photographs | JPEG | 85-90 | Standard display |
| Photographs | JPEG | 75-80 | Thumbnails |
| Photographs | WebP | 80-85 | Modern browsers |
| Graphics | PNG-8 | N/A | Simple graphics |
| Graphics | PNG-24 | N/A | Complex graphics with alpha |
| Logos | SVG | N/A | Vector logos |
| Icons | SVG | N/A | UI icons |
| Screenshots | PNG | N/A | Text clarity required |
Format Selection Matrix
| Scenario | Recommended Format | Alternative |
|---|---|---|
| Photographs | JPEG or WebP | AVIF |
| Graphics with transparency | PNG | WebP with alpha |
| Simple icons | SVG | PNG-8 |
| Animated graphics | WebP or GIF | APNG |
| High-quality prints | PNG or TIFF | JPEG at 95+ |
| Logos | SVG | PNG |
Dimension Guidelines
| Context | Max Width | Quality | Notes |
|---|---|---|---|
| Thumbnail | 200-300px | 75-80 | Small preview |
| List view | 400-600px | 80-85 | Grid displays |
| Content width | 800-1000px | 85-90 | Article content |
| Full width | 1200-1600px | 90 | Hero images |
| Lightbox/zoom | 2000-3000px | 90-95 | Detailed viewing |
| Retina thumbnail | 400-600px | 75 | 2x resolution |
| Retina content | 1600-2000px | 85 | 2x resolution |
Ruby Gem Commands
MiniMagick Basic Operations
image = MiniMagick::Image.open('source.jpg')
image.resize '800x600'
image.quality 85
image.strip
image.format 'jpg'
image.write 'output.jpg'
Vips Processing
image = Vips::Image.new_from_file('source.jpg')
resized = image.thumbnail_image(800, height: 600)
resized.jpegsave('output.jpg', Q: 85, strip: true)
ImageOptim Optimization
optimizer = ImageOptim.new(
jpegoptim: { max_quality: 85 },
optipng: { level: 6 }
)
optimizer.optimize_image!('image.jpg')
Optimization Checklist
| Task | Priority | Impact |
|---|---|---|
| Choose appropriate format | High | 30-50% size reduction |
| Remove metadata | Medium | 5-20% size reduction |
| Resize to display dimensions | High | 50-90% size reduction |
| Set optimal quality level | High | 30-70% size reduction |
| Enable progressive loading | Low | Perceived performance |
| Generate WebP variants | Medium | 25-35% size reduction |
| Implement lazy loading | Medium | Initial load time |
| Use CDN caching | High | Response time |
| Compress with tools | Medium | 10-30% size reduction |
Performance Metrics
| Metric | Target | Measurement |
|---|---|---|
| Thumbnail size | Under 20KB | File size |
| Medium image size | Under 150KB | File size |
| Large image size | Under 500KB | File size |
| Processing time | Under 2s per image | Benchmark |
| Time to first image | Under 2s | Browser timing |
| Compression ratio | 70-90% reduction | Original vs optimized |
| Cache hit rate | Above 80% | CDN analytics |