CrackedRuby CrackedRuby

Overview

Bundle optimization focuses on reducing the size and improving the efficiency of dependency bundles in Ruby applications. The bundle refers to the collection of gems and their dependencies managed by Bundler, as well as compiled assets like JavaScript and CSS in web applications. Optimization affects application startup time, memory consumption, deployment size, and runtime performance.

Ruby applications typically depend on dozens or hundreds of gems. Each gem adds code, memory overhead, and load time. A Rails application might include 150+ gems in its bundle, representing megabytes of code that must be loaded and parsed. Optimizing this bundle reduces memory footprint from hundreds of megabytes to more manageable levels, cuts startup time from seconds to milliseconds, and decreases deployment artifact sizes.

The optimization process addresses multiple layers: dependency selection at the Gemfile level, Bundler configuration and caching, code loading strategies, and asset compilation for web applications. Each layer offers different optimization opportunities with distinct trade-offs between development convenience and production performance.

# Before optimization: Loading entire gems
require 'active_support/all'
require 'rails/all'

# After optimization: Selective requires
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/keys'

Bundle optimization intersects with deployment strategies, containerization, continuous integration, and production monitoring. Optimization decisions made during development compound through the deployment pipeline, affecting build times, container image sizes, and cold start performance in serverless environments.

Key Principles

Bundle optimization operates on several core principles that govern how dependencies are managed and loaded in Ruby applications.

Dependency Minimization establishes that each gem in the bundle carries overhead beyond its primary functionality. Dependencies form transitive graphs where one gem pulls in multiple others. A single gem like Rails transitively requires 50+ gems. Minimization identifies genuinely needed dependencies versus convenience inclusions. This principle applies at both the direct dependency level in the Gemfile and the transitive dependency level through careful gem selection.

Lazy Loading defers code loading until needed rather than loading everything at application startup. Ruby's require mechanism loads and executes code immediately. Lazy loading strategies delay this execution, reducing initial memory footprint and startup time. The principle applies to optional features, conditional dependencies, and rarely-used code paths.

# Eager loading (default)
require 'nokogiri'
require 'pdf/reader'
require 'rmagick'

# Lazy loading pattern
def parse_html(content)
  require 'nokogiri' unless defined?(Nokogiri)
  Nokogiri::HTML(content)
end

def process_pdf(file)
  require 'pdf/reader' unless defined?(PDF::Reader)
  PDF::Reader.new(file)
end

Selective Inclusion recognizes that many gems provide broader functionality than applications need. Active Support includes hundreds of utility methods; applications typically use a fraction. Selective inclusion loads only required components rather than entire libraries. This principle requires understanding gem internal structure and available granular loading options.

Caching and Reuse exploits the fact that gem code remains constant across deployments and environments. Bundler caches downloaded gems and resolved dependency graphs. Build systems cache compiled assets and vendor directories. Effective caching eliminates redundant work in CI/CD pipelines and deployment processes. The principle extends to production where preloading and shared memory reduce per-process overhead.

Version Pinning and Constraints balances security updates against dependency stability. Loose version constraints allow automatic updates but risk breaking changes. Strict pinning prevents surprises but requires manual updates. Optimization requires version constraints that permit compatible updates while preventing breaking changes. The Gemfile.lock serves as the concrete version manifest.

Platform-Specific Optimization acknowledges that development, testing, and production environments have different optimization needs. Development prioritizes fast feedback and debugging capability. Production prioritizes minimal memory footprint and fast startup. Testing needs complete coverage but not production performance. Groups in the Gemfile separate environment-specific dependencies.

# Gemfile with environment optimization
gem 'rails', '~> 7.1.0'
gem 'pg', '~> 1.5'

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :development do
  gem 'bullet'
  gem 'rack-mini-profiler'
end

group :test do
  gem 'simplecov'
  gem 'webmock'
end

Asset Compilation Strategy addresses how JavaScript, CSS, images, and other static assets are bundled for web applications. Modern web applications ship megabytes of JavaScript. Optimization includes minification, tree shaking, code splitting, and compression. The principle applies whether using Sprockets, Webpacker, or modern build tools like esbuild.

Ruby Implementation

Ruby provides several mechanisms for bundle optimization through Bundler, requiring strategies, and gem loading control.

Bundler Configuration controls how gems are installed and loaded. The Gemfile defines dependencies while Bundler resolves the complete dependency graph. Configuration options affect cache behavior, parallel installation, and deployment optimization.

# .bundle/config for optimized CI/CD
BUNDLE_PATH: "vendor/bundle"
BUNDLE_WITHOUT: "development:test"
BUNDLE_DEPLOYMENT: "true"
BUNDLE_JOBS: "4"
BUNDLE_RETRY: "3"
BUNDLE_CACHE_ALL: "true"

The deployment flag forces Bundler to use the exact versions in Gemfile.lock without resolution, significantly reducing bundle install time. The jobs setting enables parallel gem installation. Caching includes all gems in the vendor/bundle directory for reuse across builds.

Selective Requiring loads only necessary components from gems. Many gems support granular requires for specific functionality.

# Loading entire Active Support
require 'active_support/all'  # Loads 100+ files

# Selective loading
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/keys'
# Loads only 10-15 files

Autoloading Configuration in Rails controls how classes are loaded on demand. Rails uses Zeitwerk for autoloading in modern versions, which loads constants as they're referenced rather than at startup.

# config/application.rb
module MyApp
  class Application < Rails::Application
    # Eager load in production, autoload in development
    config.eager_load = Rails.env.production?
    
    # Configure autoload paths
    config.autoload_paths += %W[#{config.root}/lib]
    
    # Exclude unnecessary paths
    config.autoload_paths.reject! { |p| p.include?('concerns') }
  end
end

Bootsnap Integration precompiles Ruby bytecode and caches constant lookups, reducing application boot time significantly.

# config/boot.rb
require 'bundler/setup'
require 'bootsnap/setup'

# Bootsnap caches compilation and constant resolution
# Typical boot time reduction: 50-75%

Group Management restricts gem loading to specific environments. Bundler skips groups during bundle install when configured, reducing the installed gem count.

# Install without development and test gems
# bundle install --without development test

# Require only production gems
Bundler.require(:default, Rails.env)

Custom Gem Loading implements conditional requires based on runtime needs rather than environment.

# lib/conditional_loader.rb
module ConditionalLoader
  def self.load_feature(feature_name, gem_name)
    return if @loaded_features&.include?(feature_name)
    
    require gem_name
    @loaded_features ||= Set.new
    @loaded_features << feature_name
  end
  
  def self.with_pdf_processing
    load_feature(:pdf, 'pdf/reader')
    yield if block_given?
  end
  
  def self.with_image_processing
    load_feature(:images, 'mini_magick')
    yield if block_given?
  end
end

# Usage
ConditionalLoader.with_pdf_processing do
  # PDF processing code here
end

Asset Pipeline Optimization configures Sprockets or other asset bundlers for production efficiency.

# config/environments/production.rb
Rails.application.configure do
  # Compile assets
  config.assets.compile = false
  config.assets.digest = true
  
  # Compress JavaScript and CSS
  config.assets.js_compressor = :terser
  config.assets.css_compressor = :sass
  
  # Precompile additional assets
  config.assets.precompile += %w[admin.js admin.css]
  
  # Use CDN for asset serving
  config.asset_host = 'https://cdn.example.com'
end

Dependency Auditing identifies unused or duplicate dependencies. Tools scan the bundle for gems not required by the application.

# Custom script to find unused gems
require 'bundler'

Bundler.load.specs.each do |spec|
  required_files = Dir.glob("**/*.rb").select do |file|
    File.read(file).match?(/require ['"]#{spec.name}/)
  end
  
  puts "#{spec.name}: #{required_files.empty? ? 'UNUSED' : 'USED'}"
end

Performance Considerations

Bundle optimization directly impacts multiple performance dimensions across the application lifecycle.

Startup Time represents the most immediate performance impact. Ruby applications load all required code at startup. A typical Rails application with 150 gems might spend 3-5 seconds in startup, with gem loading consuming 60-70% of that time. Optimization techniques reduce this to under 1 second.

The startup sequence involves several phases: Bundler setup (100-200ms), gem requires (2-3 seconds), Rails initialization (500ms-1s), and application code loading (variable). Each optimization targets specific phases.

# Measuring startup impact
require 'benchmark'

time = Benchmark.realtime do
  require 'bundler/setup'
  Bundler.require(:default, :production)
end

puts "Gem loading time: #{time}s"
# Before optimization: 2.8s
# After optimization: 0.6s

Memory Consumption scales with the number of loaded gems and their dependencies. Each gem allocates memory for code, constants, and data structures. A minimal Rails application might use 100-150MB of memory; a gem-heavy application can exceed 500MB. Production servers running multiple processes multiply this per-process overhead.

Optimization reduces baseline memory, allowing higher process counts on the same hardware. A 200MB reduction per process enables 2-3 additional processes on a 2GB server, directly increasing throughput capacity.

Build and Deployment Time affects CI/CD pipeline duration and deployment frequency. Installing gems, compiling assets, and creating deployment artifacts adds minutes to each deployment. Optimization through caching and selective installation reduces this overhead.

# CI optimization in .gitlab-ci.yml
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - vendor/bundle
    - node_modules
    - tmp/cache/assets

before_script:
  - bundle config set --local path 'vendor/bundle'
  - bundle install --jobs 4 --without development test
  - bundle clean

Asset Size and Load Time impacts web application perceived performance. Unoptimized JavaScript bundles often exceed 2-3MB, requiring seconds to download and parse. Optimization reduces bundle sizes to hundreds of kilobytes, improving time to interactive.

Compression ratios for JavaScript typically achieve 60-70% size reduction through minification and 70-80% through gzip. CSS compression yields similar results. Images benefit from format optimization and responsive sizing.

Dependency Resolution Time occurs during bundle install. Bundler resolves the complete dependency graph, checking version constraints and compatibility. Complex dependency graphs with many constraints require significant resolution time. Gemfile.lock eliminates resolution in production deployments.

Cache Hit Rates determine optimization effectiveness. Bundler's cache, Docker layer caching, and CI cache systems achieve different hit rates based on dependency change frequency. High hit rates (>90%) indicate stable dependencies; low rates suggest frequent updates or misconfigured caching.

# Measuring cache effectiveness
cache_stats = {
  gem_downloads: 150,
  cache_hits: 142,
  cache_misses: 8
}

hit_rate = cache_stats[:cache_hits].to_f / cache_stats[:gem_downloads]
puts "Cache hit rate: #{(hit_rate * 100).round(1)}%"
# Target: >85% for mature applications

Runtime Performance sees indirect benefits from bundle optimization. Smaller bundles reduce garbage collection pressure. Selective loading enables faster feature flags and conditional functionality. However, runtime performance optimization requires different techniques than bundle optimization.

Container Image Size directly reflects bundle efficiency in containerized deployments. Unoptimized images often exceed 1GB; optimized images achieve 200-400MB. Smaller images accelerate deployment, reduce storage costs, and improve cold start times in orchestration systems.

Multi-stage Docker builds separate build dependencies from runtime dependencies, further reducing image size.

# Multi-stage build for optimization
FROM ruby:3.2-alpine AS builder

RUN apk add --no-cache build-base postgresql-dev

WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test --jobs 4 --retry 3

FROM ruby:3.2-alpine

RUN apk add --no-cache postgresql-client

WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . .

# Final image: 250MB vs 800MB unoptimized

Practical Examples

Example 1: Rails Application Startup Optimization

A Rails API application serving JSON responses includes unnecessary gems and loads all Rails components. Optimization reduces startup time and memory footprint.

# Before: Gemfile with excess dependencies
gem 'rails', '~> 7.1.0'
gem 'pg'
gem 'puma'
gem 'devise'  # Not needed for API
gem 'kaminari'  # Not needed yet
gem 'sidekiq'  # Background jobs not used
gem 'redis'  # Only needed if using Sidekiq
gem 'aws-sdk'  # Loading entire AWS SDK

# After: Minimized Gemfile
gem 'rails', '~> 7.1.0', require: false
gem 'active_model_serializers'
gem 'pg'
gem 'puma'
gem 'aws-sdk-s3'  # Only S3, not entire SDK

# config/application.rb - selective Rails loading
require_relative 'boot'

require 'rails'
require 'active_model/railtie'
require 'active_record/railtie'
require 'action_controller/railtie'
require 'action_view/railtie'

# Skip unused components
# require 'action_mailer/railtie'
# require 'active_storage/engine'
# require 'action_cable/engine'

Bundler.require(*Rails.groups)

# Result: Startup time reduced from 4.2s to 1.1s
# Memory usage reduced from 280MB to 145MB

Example 2: Docker Image Size Optimization

A containerized application has a 1.2GB image size due to build dependencies and unnecessary files.

# Before: Single-stage build
FROM ruby:3.2

WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .

RUN bundle exec rake assets:precompile

CMD ["bundle", "exec", "puma"]

# Image size: 1.2GB

# After: Multi-stage optimized build
FROM ruby:3.2-alpine AS builder

RUN apk add --no-cache \
  build-base \
  postgresql-dev \
  nodejs \
  yarn

WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
    bundle config set --local without 'development test' && \
    bundle install --jobs 4

COPY package.json yarn.lock ./
RUN yarn install --production

COPY . .
RUN bundle exec rake assets:precompile RAILS_ENV=production

# Remove unnecessary files
RUN rm -rf node_modules spec test tmp/cache

FROM ruby:3.2-alpine

RUN apk add --no-cache \
  postgresql-client \
  tzdata

WORKDIR /app

COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /app /app

USER nobody

CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

# Image size: 340MB (72% reduction)

Example 3: CI Pipeline Caching

A GitHub Actions workflow reinstalls all gems on every run, taking 6-8 minutes per build.

# Before: No caching
name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2
      - run: bundle install
      - run: bundle exec rspec
# Build time: 6-8 minutes

# After: Optimized caching
name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2
          bundler-cache: true
      
      - name: Cache bundle
        uses: actions/cache@v3
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-
      
      - name: Bundle install
        run: |
          bundle config path vendor/bundle
          bundle install --jobs 4 --retry 3
      
      - name: Run tests
        run: bundle exec rspec

# Build time: 1.5-2 minutes (first run), 45 seconds (cached runs)

Example 4: Selective Feature Loading

An application includes PDF and image processing capabilities used by only 10% of requests. Loading these libraries at startup wastes resources.

# Before: Always loaded
# config/application.rb
require 'pdf/reader'
require 'mini_magick'

# Controllers
class DocumentsController < ApplicationController
  def process_pdf
    pdf = PDF::Reader.new(params[:file].path)
    # Process PDF
  end
  
  def process_image
    image = MiniMagick::Image.new(params[:file].path)
    # Process image
  end
end

# After: Lazy loaded
# lib/feature_loader.rb
module FeatureLoader
  def self.require_pdf
    return if defined?(PDF::Reader)
    require 'pdf/reader'
  end
  
  def self.require_image
    return if defined?(MiniMagick)
    require 'mini_magick'
  end
end

# Controllers
class DocumentsController < ApplicationController
  def process_pdf
    FeatureLoader.require_pdf
    pdf = PDF::Reader.new(params[:file].path)
    # Process PDF
  end
  
  def process_image
    FeatureLoader.require_image
    image = MiniMagick::Image.new(params[:file].path)
    # Process image
  end
end

# Result: Baseline memory reduced by 45MB
# Feature requests load libraries on-demand

Implementation Approaches

Gemfile Pruning Strategy systematically removes unnecessary dependencies. The approach starts with auditing current gems, identifying usage patterns, and eliminating unused dependencies.

The audit phase uses tools like bundle-audit and custom scripts to list all gems and their dependents. Manual code review identifies which gems the application actually requires. Test removal in a separate branch confirms no functionality breaks.

Dependencies fall into categories: core (absolutely required), optional (useful but removable), and transitive (pulled in by other gems). Core gems stay in the Gemfile. Optional gems move to conditional requires or separate groups. Transitive dependencies may warrant replacing their parent gems with lighter alternatives.

Incremental Optimization Approach implements changes progressively rather than wholesale refactoring. The approach measures baseline performance, makes one change, remeasures, and validates production stability before proceeding.

Start with high-impact, low-risk changes: enabling Bundler deployment mode, configuring CI caching, and removing obviously unused gems. Progress to medium-risk changes: selective requires, lazy loading, and group management. Reserve high-risk changes like major gem replacements for dedicated efforts.

Each change requires measurement: startup time, memory footprint, build duration, and deployment size. Establish acceptable thresholds before making changes. Roll back changes that degrade metrics unexpectedly.

Build Pipeline Optimization Strategy structures CI/CD for maximum efficiency. The approach layers caching, parallelization, and selective installation.

Configure gem caching first, using CI system cache mechanisms. Set Bundler to use vendor/bundle for local caching. Enable parallel installation with the jobs flag. Add deployment mode to skip dependency resolution.

Layer Docker caching for containerized builds. Structure Dockerfiles to copy Gemfile before application code, leveraging Docker's layer caching. Use multi-stage builds to separate build dependencies from runtime.

Implement matrix builds for testing across Ruby versions while sharing cache. Configure cache invalidation based on Gemfile.lock changes only, not source code changes.

Production Loading Strategy optimizes the production runtime environment specifically. Development and production have different needs; optimize each independently.

Production environments disable autoloading and eager load all code at startup. This trades longer startup time for faster runtime performance. Configure eager loading in Rails production configuration.

Use preloading in application servers like Puma. Preloading loads the application before forking worker processes, sharing memory pages across workers. This multiplies memory savings: a 300MB application with 4 workers uses 450MB with preloading versus 1.2GB without.

Configure production-specific gem groups that exclude development and test dependencies. Use Bundler's without flag during deployment to skip installing these groups entirely.

Dependency Substitution Strategy replaces heavy gems with lighter alternatives. The approach identifies gems with large transitive dependencies or memory footprints and finds equivalents.

Common substitutions include replacing full libraries with focused alternatives. Instead of loading all of Active Support, require only needed extensions. Replace heavy HTTP clients like RestClient with lighter alternatives like Net::HTTP for simple use cases. Use platform-standard libraries rather than gems where possible.

Substitution requires testing to ensure functional equivalence. API differences between libraries necessitate code changes. Consider the maintenance burden of less popular alternatives versus the performance gain.

Tools & Ecosystem

Bundler serves as the primary dependency management tool for Ruby applications. Version 2.3+ includes optimizations like parallel installation and improved caching. Configuration through .bundle/config controls installation behavior.

Key Bundler features for optimization include deployment mode, path configuration, frozen flag, and without groups. The audit command checks for security vulnerabilities. The outdated command identifies available updates.

# .bundle/config optimization
BUNDLE_PATH: "vendor/bundle"
BUNDLE_DEPLOYMENT: "true"
BUNDLE_JOBS: "4"
BUNDLE_RETRY: "3"
BUNDLE_WITHOUT: "development:test"

Bootsnap accelerates application boot time through compilation caching and load path optimization. The gem maintains a cache of compiled Ruby bytecode and optimizes constant lookup. Integration requires minimal configuration and typically reduces boot time by 50-75%.

# config/boot.rb
require 'bundler/setup'

# Bootsnap setup with cache in tmp directory
require 'bootsnap'
Bootsnap.setup(
  cache_dir: 'tmp/cache',
  development_mode: ENV['RAILS_ENV'] == 'development',
  load_path_cache: true,
  compile_cache_iseq: true,
  compile_cache_yaml: true
)

Derailed Benchmarks provides tooling for measuring Rails application memory and performance. The gem includes commands for memory profiling, allocation tracking, and startup time measurement.

# Measure memory usage
bundle exec derailed bundle:mem

# Measure objects allocated
bundle exec derailed bundle:objects

# Memory usage per gem
bundle exec derailed bundle:mem:each

# Startup time breakdown
bundle exec derailed exec perf:boot

Bundle Audit scans Gemfile.lock for gems with known security vulnerabilities. Regular auditing identifies security issues requiring updates. Integration into CI pipelines prevents deploying vulnerable code.

# Check for vulnerabilities
bundle audit check

# Update vulnerability database
bundle audit update

# CI integration
bundle audit check --update

Docker Layer Caching optimizes container image builds by reusing unchanged layers. Structure Dockerfiles to minimize layer rebuilds. Copy Gemfile and package.json before application code to cache dependency layers separately.

Webpack Bundle Analyzer and similar tools visualize JavaScript bundle composition for web applications. The tools identify large dependencies, duplicate code, and optimization opportunities in frontend bundles.

// webpack.config.js with analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false
    })
  ]
};

Rack Mini Profiler measures request performance in development, including gem loading time and database queries. The tool identifies slow code paths and memory allocation hotspots.

Memory Profiler and Ruby Prof provide detailed memory and CPU profiling. These tools measure actual gem impact on application performance beyond startup time.

# Memory profiling
require 'memory_profiler'

report = MemoryProfiler.report do
  require 'some_gem'
end

report.pretty_print

Dependabot and Renovate automate dependency updates. These services create pull requests for outdated gems, maintaining security while controlling update frequency. Configuration controls update schedules and grouping strategies.

Common Pitfalls

Over-Aggressive Pruning removes gems that are actually needed, causing runtime errors in production. Dependencies used in edge cases or specific environments might appear unused during testing. Always test bundle changes in staging environments matching production before deploying.

Symptoms include missing constant errors, undefined method errors for gem-provided functionality, and failures in rarely-executed code paths. Prevention requires comprehensive test coverage and production-like staging environments.

Ignoring Transitive Dependencies focuses on direct dependencies while overlooking that removing one gem might orphan unused transitive dependencies. Bundle clean removes unused gems, but developers often forget this step.

# A gem depends on nokogiri (large dependency)
gem 'some_gem'  # Requires nokogiri

# Removing some_gem doesn't automatically remove nokogiri
# Must run: bundle clean --force

Breaking Lazy Loading implements lazy requires incorrectly, causing thread safety issues or loading failures. Conditional requires inside methods work in single-threaded environments but fail under concurrency. Use thread-safe patterns with mutexes or ensure requires occur before threaded execution.

# Unsafe lazy loading
def process_data
  require 'some_gem'  # Race condition in threaded environment
  SomeGem.process
end

# Safe lazy loading
REQUIRE_MUTEX = Mutex.new

def process_data
  REQUIRE_MUTEX.synchronize do
    require 'some_gem' unless defined?(SomeGem)
  end
  SomeGem.process
end

Cache Invalidation Errors occur when CI caches become stale or corrupt, causing builds to use outdated dependencies. Cache keys based on Gemfile content rather than Gemfile.lock might miss lock file changes. Always include Gemfile.lock hash in cache keys.

Corrupted caches cause bizarre errors. Implement cache clearing strategies and monitor cache hit rates. Abnormally high hit rates might indicate stale caches.

Development-Production Parity Loss creates environments where development works but production fails. Removing gems from development that production needs, or vice versa, causes deployment failures. Maintain similar gem sets across environments, using groups only for truly environment-specific tools.

Asset Precompilation Failures happen when production asset compilation encounters missing gems or configuration. Assets might compile in development with different gems available. Always test asset precompilation in CI with production gem configuration.

# Ensure production asset compilation matches deployment
RAILS_ENV=production bundle exec rake assets:precompile

Memory Leak Introduction through improper lazy loading creates memory leaks. Loading gems inside request handlers without proper cleanup accumulates memory. Gems loading C extensions particularly risk leaks. Load such gems at startup or implement careful cleanup.

Bootsnap Cache Corruption causes intermittent errors after deploying code changes. Bootsnap caches can become invalid when code changes but cache invalidation fails. Clear Bootsnap cache during deployment or use cache-busting strategies.

# Deployment task to clear cache
namespace :deploy do
  task :clear_bootsnap do
    sh 'rm -rf tmp/cache/bootsnap*'
  end
end

Incomplete Bundle Installation in CI leaves vendor/bundle with missing gems. CI caching might restore partial bundles. Always run bundle install even with caches to ensure completeness. Use bundle check before build steps to verify bundle integrity.

Forgetting Bundle Frozen allows local Gemfile.lock changes to diverge from repository. Frozen mode prevents automatic dependency resolution, catching lock file drift. Enable frozen mode in production and CI environments.

# config/deploy.rb
set :bundle_flags, '--deployment --frozen'

Overly Strict Pinning prevents security updates by locking gems to exact versions. Use pessimistic version constraints instead of exact pins. Allow patch-level updates while preventing major version changes.

# Too strict
gem 'rails', '7.1.0'  # Blocks security patches

# Better
gem 'rails', '~> 7.1.0'  # Allows 7.1.x updates

Reference

Bundler Configuration Options

Option Description Use Case
deployment Requires Gemfile.lock, no updates Production deployments
path Install gems to specific directory CI caching, vendoring
without Skip gem groups during install Exclude dev/test in production
jobs Parallel gem installation Faster CI builds
frozen Prevent Gemfile.lock modifications Production stability
retry Retry failed gem downloads Unreliable networks
cache_all Include all gems in cache Docker builds

Performance Metrics

Metric Baseline Optimized Measurement Method
Startup Time 3-5 seconds 0.5-1 second Benchmark realtime block
Memory Footprint 250-400MB 100-150MB Process RSS measurement
Build Duration 5-10 minutes 1-2 minutes CI pipeline timing
Container Image 800MB-1.2GB 200-400MB docker images output
Gem Count 150-200 80-120 bundle list count
Asset Bundle Size 2-3MB 400-600KB Webpack stats

Optimization Checklist

Task Priority Complexity Impact
Enable deployment mode High Low Fast deploys
Configure CI caching High Low Faster builds
Remove unused gems High Medium Lower memory
Add Bootsnap High Low Faster startup
Selective requires Medium Medium Lower memory
Multi-stage Docker Medium Medium Smaller images
Lazy loading Medium High Lower baseline memory
Asset optimization High Medium Faster page loads
Parallel installation Low Low Faster installs
Group management Medium Low Environment separation

Common Bundler Commands

Command Purpose Example
bundle install Install dependencies bundle install --jobs 4
bundle update Update gems bundle update rails
bundle clean Remove unused gems bundle clean --force
bundle exec Run with bundle env bundle exec rails server
bundle list Show installed gems bundle list
bundle outdated Check for updates bundle outdated --strict
bundle audit Security scan bundle audit check
bundle config Set configuration bundle config set path vendor/bundle
bundle package Cache gems locally bundle package --all
bundle show Show gem location bundle show rails

Lazy Loading Pattern

module LazyLoader
  @loaded_features = {}
  @mutex = Mutex.new
  
  def self.load_feature(name, gem)
    return if @loaded_features[name]
    
    @mutex.synchronize do
      return if @loaded_features[name]
      require gem
      @loaded_features[name] = true
    end
  end
end

Docker Multi-Stage Template

FROM ruby:3.2-alpine AS builder
RUN apk add --no-cache build-base postgresql-dev
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment true && \
    bundle config set --local without development:test && \
    bundle install --jobs 4

FROM ruby:3.2-alpine
RUN apk add --no-cache postgresql-client
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . .
CMD ["bundle", "exec", "puma"]

Version Constraint Patterns

Pattern Meaning Example Updates Allowed
Exact Specific version only 1.2.3 None
Pessimistic Minor/patch updates ~> 1.2.0 1.2.x only
Range Version range >= 1.2, < 2.0 1.2.x to 1.9.x
Optimistic Any greater version >= 1.2.0 All newer versions