WebP reduces JPEG file sizes by 25-35% at equivalent visual quality. In Ruby, the image_processing gem is the modern choice — it wraps libvips (fast, memory-efficient) and is already a dependency in Rails 6+ Active Storage. MiniMagick is the older alternative. Both need system libraries installed.

Method 1: image_processing gem (Rails Active Storage standard)

image_processing wraps both libvips and ImageMagick. The Vips processor is faster and uses less memory than Magick for large images.

gem install image_processing
# Or in Gemfile:
# gem 'image_processing'
bundle install

# System deps
apt install libvips libvips-tools   # for Vips processor
apt install imagemagick             # for MiniMagick processor
require 'image_processing/vips'

# Single file
pipeline = ImageProcessing::Vips
  .source('photo.jpg')
  .convert('webp')
  .saver(Q: 80)  # Q = quality, 0-100

result = pipeline.call(destination: 'photo.webp')
puts "Output: #{result.path}"

# With resizing
resized_webp = ImageProcessing::Vips
  .source('photo.jpg')
  .resize_to_limit(1200, 900)  # max 1200x900, preserves aspect ratio
  .convert('webp')
  .saver(Q: 80)
  .call(destination: 'photo-resized.webp')

# Batch conversion
require 'pathname'

def batch_jpg_to_webp(src_dir, out_dir, quality: 80)
  Pathname(out_dir).mkpath
  converted = 0

  Dir.glob(File.join(src_dir, '*.{jpg,jpeg}')).each do |src|
    out = File.join(out_dir, File.basename(src, '.*') + '.webp')
    ImageProcessing::Vips
      .source(src)
      .convert('webp')
      .saver(Q: quality)
      .call(destination: out)
    converted += 1
  end
  converted
end

n = batch_jpg_to_webp('./images', './images/webp')
puts "Converted #{n} images"

libvips is significantly faster than ImageMagick for WebP encoding — about 3-5x on large images. If performance matters for batch processing, prefer the Vips processor.

Method 2: MiniMagick (familiar API, ImageMagick backend)

MiniMagick is the older Rails image processing standard. It wraps ImageMagick and handles WebP well.

gem install mini_magick
# System deps
apt install imagemagick libwebp-dev
require 'mini_magick'

def jpg_to_webp(src_path, out_path, quality: 80)
  image = MiniMagick::Image.open(src_path)
  image.format 'webp'
  image.quality quality
  # Strip EXIF metadata — reduces size and removes GPS/device info
  image.strip
  image.write out_path
end

jpg_to_webp('photo.jpg', 'photo.webp')
puts 'Done'

# MiniMagick with explicit options
MiniMagick::Tool::Convert.new do |convert|
  convert << 'photo.jpg'
  convert.define 'webp:lossless=false'
  convert.define 'webp:method=6'  # 0=fastest, 6=best compression
  convert.quality '80'
  convert << 'photo.webp'
end

The define 'webp:method=6' option tunes the WebP encoder — higher values trade speed for better compression. Method 6 is the slowest but produces the smallest files. For batch conversions where speed matters, use method 4 (default) or lower.

Method 3: ChangeThisFile API (Net::HTTP, no system libs)

The API handles WebP conversion server-side. Source auto-detected from filename — pass target=webp. Free tier: 1,000 conversions/month, no card needed.

require 'net/http'
require 'uri'
require 'securerandom'

API_KEY = 'ctf_sk_your_key_here'

def jpg_to_webp_api(src_path, out_path)
  uri = URI('https://changethisfile.com/v1/convert')
  boundary = "CTF#{SecureRandom.hex(8)}"

  file_data = File.binread(src_path)
  body = [
    "--#{boundary}\r\n",
    'Content-Disposition: form-data; name="file"; filename="' + File.basename(src_path) + "\"\r\n",
    "Content-Type: image/jpeg\r\n\r\n",
    file_data, "\r\n",
    "--#{boundary}\r\n",
    "Content-Disposition: form-data; name=\"target\"\r\n\r\n",
    "webp\r\n",
    "--#{boundary}--\r\n"
  ].join

  req = Net::HTTP::Post.new(uri)
  req['Authorization'] = "Bearer #{API_KEY}"
  req['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
  req.body = body

  resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 60) { |h| h.request(req) }
  raise "API error: #{resp.code}" unless resp.code == '200'

  File.binwrite(out_path, resp.body)
end

jpg_to_webp_api('photo.jpg', 'photo.webp')
puts 'Done'

When to use each

ApproachBest forTradeoff
image_processing (Vips)Rails 6+ Active Storage, fast batch conversionRequires libvips system package
MiniMagickOlder Rails apps, familiar ImageMagick APISlower than Vips for large batches; needs ImageMagick + libwebp
ChangeThisFile APINo system libs, Heroku/PaaS without buildpacksNetwork call per image; free tier 25MB limit

Production tips

  • Prefer image_processing/vips over image_processing/mini_magick. libvips is 3-5x faster for WebP encoding and uses significantly less memory. The API is the same — just change the require.
  • Strip EXIF data on conversion. JPEG photos often contain GPS coordinates, camera model, and other metadata that users may not want published. Strip it with image.strip in MiniMagick or via the Vips processor.
  • Serve WebP with Accept header detection. Check request.headers['Accept']&.include?('image/webp') in Rails. All modern browsers send this header. Return the WebP version to supporting clients, JPEG to others.
  • Use Active Storage variants in Rails. config.active_storage.variant_processor = :vips in application.rb enables automatic WebP output: user.avatar.variant(format: :webp, quality: 80).processed.
  • Don't delete the original JPEG. Re-encoding from WebP introduces additional quality loss. Keep both formats and serve the appropriate one.

For Rails apps, image_processing with the Vips processor is the modern standard — it's already a dependency and handles WebP natively. MiniMagick is fine for existing apps. Free API tier: 1,000 conversions/month.