CrackedRuby CrackedRuby

Resource Loading Optimization

Overview

Resource loading optimization addresses the challenge of delivering application assets—JavaScript, CSS, images, fonts, data files, and other dependencies—to users efficiently. The optimization process balances multiple competing factors: initial page load speed, time to interactive, total bandwidth consumption, caching effectiveness, and perceived performance.

Modern applications typically consist of hundreds of individual resources. A single web page might reference 50-100+ JavaScript files, dozens of stylesheets, multiple font files, numerous images, and various data endpoints. Without optimization, loading these resources creates network waterfalls where each resource blocks subsequent resources, leading to slow load times and poor user experience.

Resource loading optimization operates at multiple levels. At the network level, techniques like HTTP/2 multiplexing, connection reuse, and domain sharding affect how resources transfer. At the application level, bundling, code splitting, and lazy loading determine which resources load and when. At the content level, minification, compression, and image optimization reduce resource size. At the caching level, proper cache headers and versioning strategies determine whether resources require network requests.

The optimization challenge intensifies with modern application architecture. Single-page applications (SPAs) often ship megabytes of JavaScript to support rich interactivity. Progressive web applications require offline capabilities through service workers. Mobile applications must handle intermittent connectivity and limited bandwidth. Each context demands different optimization strategies.

# Example: Basic resource loading without optimization
class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    @reviews = @product.reviews  # Loads all reviews
    @related_products = Product.similar_to(@product)  # N+1 query risk
    @images = @product.images  # Loads all image records
  end
end

This unoptimized approach loads all related data immediately, regardless of whether users need it. The reviews might not display above the fold. Related products might appear at the bottom of the page. Images might not be visible initially. Each unnecessary load increases time to interactive.

Key Principles

Resource loading optimization rests on several fundamental principles that guide implementation decisions across different contexts and technologies.

Critical rendering path optimization focuses on identifying and prioritizing resources required for initial page render. The browser cannot display content until it constructs the DOM tree from HTML and the CSSOM tree from CSS. JavaScript that modifies the DOM or CSSOM blocks rendering. Optimization requires identifying the minimal set of resources needed for above-the-fold content and deferring everything else.

The critical rendering path follows a specific sequence: parse HTML to build DOM, encounter CSS and build CSSOM, execute synchronous JavaScript that might modify DOM/CSSOM, combine DOM and CSSOM into render tree, calculate layout, paint pixels. Each blocking resource extends this path. A single blocking CSS file delays all rendering. A synchronous script blocks HTML parsing until it executes.

Lazy loading defers resource loading until needed. Instead of loading all resources upfront, applications load resources when they become visible, when users navigate to specific sections, or when specific functionality activates. This reduces initial payload size and network requests, improving time to interactive.

Lazy loading operates through several mechanisms. Intersection Observer API detects when elements enter the viewport, triggering image or component loads. Route-based code splitting loads JavaScript bundles only for visited routes. Dynamic imports load modules on-demand. Data pagination fetches records as users scroll. Each mechanism trades initial load speed for slightly delayed access to non-critical content.

Caching strategies determine how browsers and intermediary servers store and reuse resources. Effective caching eliminates network requests entirely for cached resources, providing the fastest possible load time. Cache strategies must balance freshness requirements against performance benefits.

Browser caching operates through HTTP headers. Cache-Control headers specify caching duration and conditions. ETags enable conditional requests where servers respond with 304 Not Modified if content unchanged. Immutable assets—files with hashed filenames—can cache indefinitely since new versions use different filenames. HTML typically requires revalidation since it references other cached resources.

Bundle optimization addresses JavaScript and CSS payload size through splitting, tree-shaking, and code organization. Modern applications ship substantial JavaScript, often megabytes uncompressed. Bundle optimization techniques reduce download size and improve parse/compile performance.

Code splitting divides applications into multiple bundles loaded separately. Route-based splitting loads bundles per route. Component-based splitting loads bundles per major component. Vendor splitting separates application code from library code, enabling better caching since libraries change less frequently than application code.

Tree-shaking eliminates unused code during build process. When importing from libraries, tree-shaking includes only imported functions, excluding unused code. This requires ES6 module syntax since static analysis determines which exports the application uses. Libraries supporting tree-shaking show significant size reductions—importing a single lodash function includes only that function rather than the entire library.

Resource prioritization controls loading order for resources with different importance levels. Browsers implement default prioritization, but applications can influence priorities through resource hints and loading attributes.

Resource hints provide browsers with loading guidance. dns-prefetch resolves domains before requests. preconnect establishes connections to domains. prefetch loads resources likely needed for future navigation. preload loads resources required for current navigation with high priority. Each hint type serves specific use cases and timing requirements.

Progressive enhancement structures applications so core functionality works with minimal resources, with enhanced features loading progressively. This ensures applications remain usable even when resource loading fails or completes slowly. The base experience requires only critical HTML and CSS, with JavaScript adding interactivity as it loads.

Compression reduces resource size through various algorithms. Gzip and Brotli compress text resources (HTML, CSS, JavaScript, JSON) by 60-80%. Images use format-specific compression. Videos use codec-specific compression. Each resource type requires appropriate compression techniques.

Ruby Implementation

Ruby web applications, particularly those built with Rails, implement resource loading optimization through several frameworks and conventions. The Asset Pipeline, Webpacker, and modern build tools provide infrastructure for optimization strategies.

The Asset Pipeline, introduced in Rails 3.1, provides a framework for concatenating and minifying JavaScript and CSS assets. It processes assets through a series of transformations, applies cache-busting fingerprints, and serves optimized versions in production.

# config/environments/production.rb
Rails.application.configure do
  # Compile assets during deployment
  config.assets.compile = false
  
  # Enable asset digests for cache busting
  config.assets.digest = true
  
  # Compress CSS and JavaScript
  config.assets.css_compressor = :sass
  config.assets.js_compressor = :terser
  
  # Set far-future expires headers for fingerprinted assets
  config.public_file_server.enabled = true
  config.public_file_server.headers = {
    'Cache-Control' => 'public, max-age=31536000, immutable'
  }
end

Asset organization follows Rails conventions. Files in app/assets/javascripts and app/assets/stylesheets compile into bundles. The manifest file specifies which files to include:

// app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require_tree .

This manifest compiles all JavaScript files in the directory tree into a single application.js bundle. In production, Rails generates a fingerprinted filename like application-a1b2c3d4e5.js. The fingerprint changes when content changes, busting caches automatically.

Sprockets, the Asset Pipeline engine, supports preprocessors for CoffeeScript, Sass, ERB, and other languages. Files process through multiple preprocessor stages based on extensions. A file named styles.css.scss.erb processes through ERB first, then Sass, producing CSS output.

For lazy loading data in Ruby applications, Active Record provides query methods that defer loading until accessed:

class ProductsController < ApplicationController
  def show
    @product = Product.includes(:reviews, :images).find(params[:id])
    
    # Lazy load related products only if requested
    @related_products = Product.where(category: @product.category)
                              .where.not(id: @product.id)
                              .limit(5)
                              .lazy
  end
  
  def related
    product = Product.find(params[:id])
    @related = product.similar_products
    render partial: 'related_products', locals: { products: @related }
  end
end

The includes method eager loads associations, preventing N+1 queries. The lazy method returns an enumerator that loads records on-demand rather than immediately. The separate related action enables client-side code to load related products asynchronously.

Rails provides view helpers for optimized asset loading:

<%# Critical CSS inline in head %>
<%= content_tag :style, critical_css %>

<%# Preload fonts %>
<%= preload_link_tag asset_path('custom-font.woff2'), as: 'font', type: 'font/woff2', crossorigin: true %>

<%# Defer non-critical JavaScript %>
<%= javascript_include_tag 'application', defer: true %>

<%# Lazy load images %>
<%= image_tag @product.image_url, loading: 'lazy' %>

For more granular control, Rails applications integrate with modern JavaScript bundlers. Webpacker, the default JavaScript compiler in Rails 6, uses webpack for advanced optimization:

// config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

// Enable code splitting
environment.splitChunks((config) => ({
  ...config,
  cacheGroups: {
    vendor: {
      test: /node_modules/,
      name: 'vendor',
      chunks: 'all'
    }
  }
}))

// Tree shaking configuration
environment.config.optimization = {
  ...environment.config.optimization,
  usedExports: true,
  sideEffects: false
}

module.exports = environment

Dynamic imports in JavaScript enable route-based code splitting:

// app/javascript/packs/application.js
document.addEventListener('turbolinks:load', () => {
  const element = document.querySelector('[data-behavior="chart"]')
  
  if (element) {
    // Load charting library only when needed
    import('chart.js').then((Chart) => {
      new Chart.default(element, {
        type: element.dataset.chartType,
        data: JSON.parse(element.dataset.chartData)
      })
    })
  }
})

Rails caching strategies optimize server-side rendering performance:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    
    # Fragment caching for expensive partials
    # Cache key automatically busts when product updated
    fresh_when(@product)
  end
end
<%# views/products/show.html.erb %>
<% cache @product do %>
  <%= render 'product_details', product: @product %>
<% end %>

<% cache ['reviews', @product, @product.reviews.maximum(:updated_at)] do %>
  <%= render 'reviews', reviews: @product.reviews %>
<% end %>

HTTP caching headers in Rails controllers:

class AssetsController < ApplicationController
  def show
    @asset = Asset.find(params[:id])
    
    # Browser can cache for 1 hour
    expires_in 1.hour, public: true
    
    # Set ETag for conditional requests
    if stale?(etag: @asset, last_modified: @asset.updated_at)
      send_file @asset.file_path
    end
  end
end

Implementation Approaches

Resource loading optimization requires choosing between multiple implementation strategies based on application requirements, user base characteristics, and technical constraints.

Bundling strategy determines how to package JavaScript and CSS. Monolithic bundling creates single bundles for all application code, minimizing HTTP requests but increasing initial download size. Code splitting creates multiple bundles loaded based on need, reducing initial size but increasing request counts. Hybrid approaches balance these tradeoffs.

Monolithic bundling works well for small to medium applications where total JavaScript size remains under 200-300KB compressed. Single bundle loading avoids connection overhead and reduces complexity. Modern HTTP/2 and HTTP/3 multiplexing reduces the traditional advantage of bundling, but single bundles still benefit from simpler caching strategies and reduced server load.

# Monolithic approach configuration
# config/webpack/environment.js
environment.config.optimization = {
  splitChunks: false,  # Single bundle
  runtimeChunk: false
}

Route-based code splitting loads JavaScript bundles per application route. When users navigate to a route, the application loads that route's bundle. This reduces initial bundle size significantly for large applications with many routes. The tradeoff includes increased build complexity and potential loading delays during navigation.

# Route-based splitting setup
# app/javascript/routes/index.js
export default {
  '/products': () => import('./products'),
  '/users': () => import('./users'),
  '/dashboard': () => import('./dashboard')
}

# Router initialization
document.addEventListener('DOMContentLoaded', () => {
  const path = window.location.pathname
  const loader = routes[path]
  
  if (loader) {
    loader().then(module => module.init())
  }
})

Component-based splitting loads bundles for specific features rather than routes. Large components like editors, data visualization libraries, or media players load only when needed. This approach requires identifying high-impact components that significantly affect bundle size.

Loading timing strategy controls when resources load relative to page lifecycle. Synchronous loading blocks page processing until resources complete. Asynchronous loading allows page processing to continue. Deferred loading postpones execution until page parsing completes.

Critical resource synchronous loading ensures render-blocking resources load immediately:

<%# Critical CSS loads synchronously in head %>
<head>
  <%= stylesheet_link_tag 'critical', media: 'all' %>
  <%= javascript_include_tag 'critical', defer: false %>
</head>

Asynchronous loading prevents blocking but requires handling loading states:

// Async module loading with loading state
class FeatureLoader {
  constructor(modulePath) {
    this.modulePath = modulePath
    this.loading = false
    this.loaded = false
    this.module = null
  }
  
  async load() {
    if (this.loaded) return this.module
    if (this.loading) return this.waitForLoad()
    
    this.loading = true
    this.showLoadingState()
    
    try {
      this.module = await import(this.modulePath)
      this.loaded = true
      return this.module
    } finally {
      this.loading = false
      this.hideLoadingState()
    }
  }
  
  waitForLoad() {
    return new Promise((resolve) => {
      const checkLoaded = () => {
        if (this.loaded) {
          resolve(this.module)
        } else {
          setTimeout(checkLoaded, 50)
        }
      }
      checkLoaded()
    })
  }
}

Caching level strategy determines where and how long to cache resources. Browser caching, CDN caching, and server-side caching operate at different layers with different characteristics.

Immutable asset caching works for fingerprinted resources:

# config/environments/production.rb
config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000, immutable',
  'Expires' => 1.year.from_now.httpdate
}

HTML typically requires revalidation to reference updated assets:

class PagesController < ApplicationController
  def show
    response.headers['Cache-Control'] = 'public, max-age=0, must-revalidate'
    response.headers['ETag'] = Digest::MD5.hexdigest("#{page_version}-#{asset_version}")
  end
end

Preloading strategy uses resource hints to influence browser loading priorities. Preloading critical resources improves time to interactive. Prefetching likely-needed resources improves navigation performance.

<%# Preload critical resources %>
<%= preload_link_tag font_path('icons.woff2'), as: 'font', type: 'font/woff2', crossorigin: true %>
<%= preload_link_tag asset_path('hero-image.jpg'), as: 'image' %>

<%# Prefetch likely next navigation %>
<link rel="prefetch" href="<%= products_path %>" as="document">

Data loading strategy determines how to fetch API data. Eager loading fetches all data upfront. Lazy loading fetches data when needed. Pagination loads data in chunks. Infinite scroll loads data progressively.

# GraphQL approach with query-level optimization
class ProductsQuery < Types::BaseObject
  field :products, [Types::ProductType], null: false do
    argument :limit, Integer, required: false, default_value: 20
    argument :offset, Integer, required: false, default_value: 0
  end
  
  def products(limit:, offset:)
    # Batch load products
    Product.includes(:images)
           .limit(limit)
           .offset(offset)
  end
end

Common Patterns

Several established patterns address recurring resource loading optimization challenges. These patterns provide proven solutions for common scenarios.

Lazy loading with intersection observer defers image and component loading until they enter the viewport:

// Lazy loading service
class LazyLoader {
  constructor(options = {}) {
    this.options = {
      rootMargin: '50px',
      threshold: 0.01,
      ...options
    }
    
    this.observer = new IntersectionObserver(
      (entries) => this.handleIntersection(entries),
      this.options
    )
  }
  
  observe(element) {
    this.observer.observe(element)
  }
  
  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        this.loadElement(entry.target)
        this.observer.unobserve(entry.target)
      }
    })
  }
  
  loadElement(element) {
    if (element.dataset.src) {
      element.src = element.dataset.src
      element.removeAttribute('data-src')
    }
    
    if (element.dataset.component) {
      this.loadComponent(element.dataset.component, element)
    }
  }
  
  async loadComponent(componentName, element) {
    const module = await import(`./components/${componentName}`)
    const component = new module.default(element)
    component.render()
  }
}

// Usage
document.addEventListener('DOMContentLoaded', () => {
  const loader = new LazyLoader()
  document.querySelectorAll('[data-lazy]').forEach(el => loader.observe(el))
})
<%# View template with lazy loading %>
<img data-lazy 
     data-src="<%= @product.image_url %>" 
     alt="<%= @product.name %>"
     class="lazy-image">

<div data-lazy 
     data-component="ReviewsWidget" 
     data-product-id="<%= @product.id %>">
</div>

Progressive image loading displays low-quality placeholders while high-quality images load:

class ImageOptimizer
  def self.generate_variants(image_path)
    image = MiniMagick::Image.open(image_path)
    
    # Generate low-quality placeholder (LQIP)
    placeholder = image.clone
    placeholder.resize "20x20"
    placeholder.quality "10"
    placeholder_path = image_path.sub(/(\.\w+)$/, '-placeholder\1')
    placeholder.write(placeholder_path)
    
    # Generate responsive variants
    variants = {}
    [400, 800, 1200, 1600].each do |width|
      variant = image.clone
      variant.resize "#{width}x"
      variant.quality "85"
      variant_path = image_path.sub(/(\.\w+)$/, "-#{width}w\\1")
      variant.write(variant_path)
      variants[width] = variant_path
    end
    
    { placeholder: placeholder_path, variants: variants }
  end
end
// Progressive image component
class ProgressiveImage {
  constructor(element) {
    this.element = element
    this.placeholder = element.dataset.placeholder
    this.fullSize = element.dataset.src
    this.loaded = false
  }
  
  load() {
    // Show placeholder immediately
    this.element.src = this.placeholder
    this.element.classList.add('loading')
    
    // Load full-size image
    const img = new Image()
    img.onload = () => {
      this.element.src = this.fullSize
      this.element.classList.remove('loading')
      this.element.classList.add('loaded')
      this.loaded = true
    }
    img.src = this.fullSize
  }
}

Route-based code splitting with loading states handles navigation between different application sections:

// Route loader with transition states
class RouteLoader {
  constructor(routes) {
    this.routes = routes
    this.currentRoute = null
    this.cache = new Map()
  }
  
  async navigate(path) {
    const loader = this.routes[path]
    if (!loader) {
      throw new Error(`No route found for ${path}`)
    }
    
    // Show loading state
    this.showLoadingIndicator()
    
    try {
      // Check cache first
      let module = this.cache.get(path)
      
      if (!module) {
        // Load module if not cached
        module = await loader()
        this.cache.set(path, module)
      }
      
      // Unmount current route
      if (this.currentRoute) {
        await this.currentRoute.unmount()
      }
      
      // Mount new route
      await module.mount()
      this.currentRoute = module
      
      history.pushState({}, '', path)
    } catch (error) {
      this.showError(error)
    } finally {
      this.hideLoadingIndicator()
    }
  }
  
  showLoadingIndicator() {
    document.body.classList.add('route-loading')
  }
  
  hideLoadingIndicator() {
    document.body.classList.remove('route-loading')
  }
}

Resource bundling with vendor splitting separates application code from library code:

// webpack configuration for vendor splitting
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    runtimeChunk: 'single'
  }
}

Critical CSS extraction inlines critical styles while deferring non-critical CSS:

# lib/critical_css_extractor.rb
class CriticalCssExtractor
  def self.extract(url)
    require 'critical'
    
    Critical.generate({
      base: 'public/',
      src: url,
      target: {
        html: 'index.html',
        css: 'critical.css'
      },
      width: 1300,
      height: 900,
      inline: true,
      extract: true
    })
  end
end
<%# Inline critical CSS %>
<head>
  <style><%= critical_css_for_route(@route) %></style>
  
  <%# Defer non-critical CSS %>
  <link rel="preload" 
        href="<%= asset_path('application.css') %>" 
        as="style" 
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript>
    <%= stylesheet_link_tag 'application' %>
  </noscript>
</head>

Incremental static regeneration combines static generation with on-demand updates:

class StaticPageCache
  TTL = 1.hour
  
  def self.get_or_generate(page_key)
    cached = Rails.cache.read(page_key)
    
    if cached && cached[:generated_at] > TTL.ago
      return cached[:content]
    end
    
    # Serve stale content while regenerating
    Thread.new { regenerate(page_key) } if cached
    
    cached ? cached[:content] : regenerate(page_key)
  end
  
  def self.regenerate(page_key)
    content = generate_page_content(page_key)
    Rails.cache.write(page_key, {
      content: content,
      generated_at: Time.current
    })
    content
  end
end

Performance Considerations

Resource loading optimization directly impacts application performance metrics including First Contentful Paint (FCP), Largest Contentful Paint (LCP), Time to Interactive (TTI), and Total Blocking Time (TBT). Understanding performance characteristics guides optimization decisions.

Bundle size impact affects parse, compile, and execution time. JavaScript engines must parse and compile code before execution. Large bundles increase parse time, delaying interactivity. Modern devices parse approximately 1MB of JavaScript per second. A 3MB uncompressed bundle requires 3+ seconds just for parsing, excluding network transfer time.

Minimizing bundle size through tree-shaking and code splitting reduces parse overhead:

# webpack configuration for size optimization
module.exports = {
  optimization: {
    usedExports: true,
    sideEffects: false,
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log']
          }
        }
      })
    ]
  }
}

Bundle analysis identifies optimization opportunities:

// webpack-bundle-analyzer integration
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
}

Network transfer overhead includes connection establishment, TLS handshake, and data transfer. HTTP/2 reduces connection overhead through multiplexing but cannot eliminate transfer time. Compression reduces transfer time for text resources but requires CPU for decompression.

Brotli compression typically achieves 15-20% better compression than gzip for text resources:

# Rack middleware for Brotli compression
class BrotliCompression
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    if should_compress?(env, headers)
      compressed_body = compress_body(body)
      headers['Content-Encoding'] = 'br'
      headers['Content-Length'] = compressed_body.bytesize.to_s
      [status, headers, [compressed_body]]
    else
      [status, headers, body]
    end
  end
  
  private
  
  def should_compress?(env, headers)
    env['HTTP_ACCEPT_ENCODING']&.include?('br') &&
    compressible_content_type?(headers['Content-Type'])
  end
  
  def compress_body(body)
    content = ''
    body.each { |part| content << part }
    Brotli.deflate(content)
  end
end

Caching effectiveness determines how many resources require network requests. Perfect caching eliminates network requests entirely. Cache hit rates of 90%+ significantly improve load performance for returning visitors.

Measuring cache effectiveness:

class CacheMetrics
  def self.record_cache_hit(resource_type)
    Rails.cache.increment("cache_hits:#{resource_type}")
  end
  
  def self.record_cache_miss(resource_type)
    Rails.cache.increment("cache_misses:#{resource_type}")
  end
  
  def self.hit_rate(resource_type)
    hits = Rails.cache.read("cache_hits:#{resource_type}") || 0
    misses = Rails.cache.read("cache_misses:#{resource_type}") || 0
    total = hits + misses
    
    total > 0 ? (hits.to_f / total * 100).round(2) : 0
  end
end

Image optimization often provides largest performance gains since images typically constitute 50-70% of page weight. Modern image formats like WebP and AVIF offer 25-50% better compression than JPEG and PNG.

Responsive image implementation:

class ResponsiveImageGenerator
  FORMATS = ['webp', 'jpg']
  SIZES = [400, 800, 1200, 1600, 2400]
  
  def self.generate_srcset(original_path)
    FORMATS.map do |format|
      sources = SIZES.map do |width|
        optimized_path = optimize_image(original_path, width, format)
        "#{optimized_path} #{width}w"
      end.join(', ')
      
      { format: format, srcset: sources }
    end
  end
  
  def self.optimize_image(path, width, format)
    image = MiniMagick::Image.open(path)
    image.format(format)
    image.resize("#{width}x")
    image.quality(format == 'webp' ? 80 : 85)
    
    output_path = path.sub(/\.\w+$/, "-#{width}w.#{format}")
    image.write(output_path)
    output_path
  end
end

Lazy loading performance trades initial load speed for potential delays when content becomes visible. Intersection Observer efficiently detects visibility, but loading delays might become perceptible if not anticipated correctly.

Prefetching for smoother lazy loading:

class PredictiveLazyLoader {
  constructor(options = {}) {
    this.observer = new IntersectionObserver(
      (entries) => this.handleIntersection(entries),
      {
        rootMargin: '200px',  // Start loading before visible
        threshold: 0.01,
        ...options
      }
    )
    
    this.loadQueue = []
    this.loading = false
  }
  
  async handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        this.queueLoad(entry.target)
        this.observer.unobserve(entry.target)
      }
    })
    
    if (!this.loading) {
      this.processQueue()
    }
  }
  
  queueLoad(element) {
    this.loadQueue.push(element)
  }
  
  async processQueue() {
    if (this.loadQueue.length === 0) {
      this.loading = false
      return
    }
    
    this.loading = true
    const element = this.loadQueue.shift()
    
    await this.loadElement(element)
    
    // Continue processing with delay to avoid blocking
    requestIdleCallback(() => this.processQueue())
  }
}

Server-side rendering (SSR) performance impacts time to first byte and time to interactive. SSR improves FCP by sending rendered HTML immediately but can delay TTI if JavaScript hydration takes significant time.

Streaming SSR reduces time to first byte:

class StreamingRenderer
  def render_stream(template, context)
    Enumerator.new do |yielder|
      # Send HTML head immediately
      yielder << render_partial('layouts/head', context)
      
      # Stream content as it becomes available
      context.each_chunk do |chunk_data|
        yielder << render_partial(chunk_data[:partial], chunk_data)
      end
      
      # Close HTML
      yielder << render_partial('layouts/footer', context)
    end
  end
end

Practical Examples

These examples demonstrate resource loading optimization in realistic scenarios, showing complete implementations with context.

E-commerce product page optimization requires balancing immediate content display with deferred resource loading for reviews, recommendations, and secondary images:

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def show
    @product = Product.includes(:primary_image)
                     .find(params[:id])
    
    # Set caching headers
    fresh_when(@product, public: true)
    expires_in 1.hour, public: true
    
    # Preload critical resources
    response.headers['Link'] = [
      "<#{@product.primary_image.url}>; rel=preload; as=image",
      "<#{asset_path('product.js')}>; rel=preload; as=script"
    ].join(', ')
  end
  
  def reviews
    product = Product.find(params[:id])
    @reviews = product.reviews
                     .includes(:user)
                     .order(created_at: :desc)
                     .page(params[:page])
    
    render partial: 'reviews', locals: { reviews: @reviews }
  end
  
  def recommendations
    product = Product.find(params[:id])
    @recommendations = RecommendationEngine
                        .similar_products(product, limit: 6)
    
    render json: @recommendations.map { |p|
      {
        id: p.id,
        name: p.name,
        price: p.price,
        image_url: p.primary_image.url
      }
    }
  end
end
<%# app/views/products/show.html.erb %>
<!DOCTYPE html>
<html>
<head>
  <%# Critical CSS inline %>
  <style><%= render 'critical_css' %></style>
  
  <%# Preload hero image %>
  <%= preload_link_tag @product.primary_image.url, as: 'image' %>
  
  <%# Defer non-critical CSS %>
  <%= stylesheet_link_tag 'application', 
                          media: 'print', 
                          onload: "this.media='all'" %>
</head>
<body>
  <%# Above-the-fold content loads immediately %>
  <div class="product-hero">
    <%= image_tag @product.primary_image.url, 
                  alt: @product.name,
                  width: 800,
                  height: 600 %>
    
    <div class="product-info">
      <h1><%= @product.name %></h1>
      <p class="price"><%= number_to_currency(@product.price) %></p>
      <button class="add-to-cart" data-product-id="<%= @product.id %>">
        Add to Cart
      </button>
    </div>
  </div>
  
  <%# Lazy load gallery %>
  <div data-lazy-component="ProductGallery" 
       data-product-id="<%= @product.id %>">
  </div>
  
  <%# Lazy load reviews %>
  <div id="reviews" data-lazy-load="true">
    <div class="loading-placeholder">Loading reviews...</div>
  </div>
  
  <%# Lazy load recommendations %>
  <div id="recommendations" data-lazy-load="true">
    <div class="loading-placeholder">Loading recommendations...</div>
  </div>
  
  <%= javascript_include_tag 'product', defer: true %>
</body>
</html>
// app/javascript/product.js
class ProductPageLoader {
  constructor() {
    this.initializeLazyLoading()
    this.setupIntersectionObserver()
  }
  
  initializeLazyLoading() {
    this.componentsLoaded = new Set()
    this.observer = new IntersectionObserver(
      (entries) => this.handleIntersection(entries),
      { rootMargin: '100px' }
    )
    
    document.querySelectorAll('[data-lazy-load]').forEach(el => {
      this.observer.observe(el)
    })
  }
  
  async handleIntersection(entries) {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        await this.loadComponent(entry.target)
        this.observer.unobserve(entry.target)
      }
    }
  }
  
  async loadComponent(element) {
    const componentType = element.id
    
    if (this.componentsLoaded.has(componentType)) return
    this.componentsLoaded.add(componentType)
    
    try {
      switch(componentType) {
        case 'reviews':
          await this.loadReviews(element)
          break
        case 'recommendations':
          await this.loadRecommendations(element)
          break
      }
    } catch (error) {
      console.error(`Failed to load ${componentType}:`, error)
      element.innerHTML = '<p>Failed to load content</p>'
    }
  }
  
  async loadReviews(element) {
    const productId = document.querySelector('[data-product-id]')
                             .dataset.productId
    
    const response = await fetch(`/products/${productId}/reviews`)
    const html = await response.text()
    element.innerHTML = html
  }
  
  async loadRecommendations(element) {
    const productId = document.querySelector('[data-product-id]')
                             .dataset.productId
    
    const response = await fetch(`/products/${productId}/recommendations`)
    const products = await response.json()
    
    // Load recommendation component dynamically
    const { RecommendationsWidget } = await import('./components/recommendations')
    const widget = new RecommendationsWidget(element, products)
    widget.render()
  }
  
  setupIntersectionObserver() {
    // Prefetch images as they approach viewport
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target
          img.src = img.dataset.src
          img.removeAttribute('data-src')
          imageObserver.unobserve(img)
        }
      })
    }, { rootMargin: '50px' })
    
    document.querySelectorAll('img[data-src]').forEach(img => {
      imageObserver.observe(img)
    })
  }
}

document.addEventListener('DOMContentLoaded', () => {
  new ProductPageLoader()
})

Dashboard with data visualization demonstrates progressive loading for computation-heavy components:

class DashboardController < ApplicationController
  def index
    # Load minimal initial data
    @summary = {
      total_sales: Rails.cache.fetch('dashboard:total_sales', expires_in: 5.minutes) {
        Order.sum(:total)
      },
      order_count: Rails.cache.fetch('dashboard:order_count', expires_in: 5.minutes) {
        Order.count
      }
    }
  end
  
  def chart_data
    chart_type = params[:type]
    date_range = parse_date_range(params[:range])
    
    data = case chart_type
    when 'sales'
      generate_sales_data(date_range)
    when 'traffic'
      generate_traffic_data(date_range)
    when 'conversion'
      generate_conversion_data(date_range)
    end
    
    render json: data
  end
  
  private
  
  def generate_sales_data(date_range)
    Rails.cache.fetch("chart:sales:#{date_range.first}:#{date_range.last}", 
                      expires_in: 30.minutes) {
      Order.where(created_at: date_range)
           .group_by_day(:created_at)
           .sum(:total)
    }
  end
end
// app/javascript/dashboard.js
class DashboardLoader {
  constructor() {
    this.chartsLoaded = {}
    this.initializeCharts()
  }
  
  async initializeCharts() {
    const charts = document.querySelectorAll('[data-chart-type]')
    
    // Load charts sequentially to avoid overwhelming server
    for (const chartElement of charts) {
      await this.loadChart(chartElement)
      await this.delay(200)  // Brief delay between requests
    }
  }
  
  async loadChart(element) {
    const chartType = element.dataset.chartType
    const dateRange = element.dataset.dateRange || '7d'
    
    try {
      // Show loading state
      element.innerHTML = '<div class="chart-loading">Loading...</div>'
      
      // Dynamically import Chart.js only when needed
      const Chart = await this.loadChartLibrary()
      
      // Fetch data
      const data = await this.fetchChartData(chartType, dateRange)
      
      // Render chart
      const canvas = document.createElement('canvas')
      element.innerHTML = ''
      element.appendChild(canvas)
      
      new Chart(canvas, {
        type: this.getChartType(chartType),
        data: data,
        options: this.getChartOptions(chartType)
      })
      
      this.chartsLoaded[chartType] = true
    } catch (error) {
      console.error(`Failed to load chart ${chartType}:`, error)
      element.innerHTML = '<div class="chart-error">Failed to load chart</div>'
    }
  }
  
  async loadChartLibrary() {
    if (!this.Chart) {
      const module = await import('chart.js/auto')
      this.Chart = module.default
    }
    return this.Chart
  }
  
  async fetchChartData(type, range) {
    const response = await fetch(`/dashboard/chart_data?type=${type}&range=${range}`)
    if (!response.ok) throw new Error('Failed to fetch chart data')
    return response.json()
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

Mobile application with offline support implements service worker caching for critical resources:

// public/service-worker.js
const CACHE_VERSION = 'v1'
const CRITICAL_CACHE = `critical-${CACHE_VERSION}`
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`

const CRITICAL_ASSETS = [
  '/',
  '/offline',
  '/assets/application.css',
  '/assets/application.js',
  '/assets/logo.png'
]

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CRITICAL_CACHE)
      .then(cache => cache.addAll(CRITICAL_ASSETS))
  )
})

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        if (cachedResponse) {
          // Return cached version and update in background
          fetchAndCache(event.request)
          return cachedResponse
        }
        
        return fetchAndCache(event.request)
      })
      .catch(() => {
        // Return offline page for navigation requests
        if (event.request.mode === 'navigate') {
          return caches.match('/offline')
        }
      })
  )
})

async function fetchAndCache(request) {
  const response = await fetch(request)
  
  if (response.ok && request.method === 'GET') {
    const cache = await caches.open(DYNAMIC_CACHE)
    cache.put(request, response.clone())
  }
  
  return response
}

Reference

Resource Loading Strategies

Strategy Best For Load Timing Bundle Impact
Synchronous Critical CSS, polyfills Blocks rendering Minimal
Asynchronous Analytics, third-party scripts Non-blocking Minimal
Deferred Non-critical JavaScript After parsing Moderate
Lazy Below-fold content On visibility Significant reduction
Prefetch Likely next navigation Idle time Improved subsequent loads
Preload Current page critical resources High priority Faster critical rendering

HTTP Cache Headers

Header Purpose Example Value Use Case
Cache-Control Caching directives public, max-age=31536000 Control caching behavior
ETag Resource version identifier W/"a1b2c3d4" Conditional requests
Expires Absolute expiration time Thu, 01 Dec 2025 16:00:00 GMT Legacy cache control
Last-Modified Resource modification time Wed, 21 Oct 2025 07:28:00 GMT Conditional requests
Vary Cache key dependencies Accept-Encoding, User-Agent Vary cache by headers

Code Splitting Patterns

Pattern Implementation Benefits Tradeoffs
Route-based Import per route Smaller initial bundle Navigation delay
Component-based Import large components Reduced unused code Component load delay
Vendor splitting Separate libraries Better caching Multiple requests
Dynamic import On-demand loading Maximum flexibility Complex state management

Image Optimization Formats

Format Best For Compression Browser Support
AVIF Photos, complex images Excellent Modern browsers
WebP Photos, graphics Very good Widespread
JPEG Photos without transparency Good Universal
PNG Graphics with transparency Fair Universal
SVG Icons, logos, illustrations Excellent for vector Universal

Performance Metrics

Metric Description Target Impact
FCP First Contentful Paint Under 1.8s Perceived load speed
LCP Largest Contentful Paint Under 2.5s Main content visibility
TTI Time to Interactive Under 3.8s Usability
TBT Total Blocking Time Under 300ms Responsiveness
CLS Cumulative Layout Shift Under 0.1 Visual stability

Resource Priority Levels

Priority Resource Type Loading Behavior Example
Highest Critical CSS, preload Load immediately Above-fold styles
High Synchronous scripts Blocks parsing Core functionality
Medium Visible images Load with priority Hero images
Low Async scripts, prefetch Load when possible Analytics
Lowest Lazy resources Load on demand Below-fold content

Compression Techniques

Technique Resource Type Size Reduction CPU Cost
Brotli Text resources 15-25% vs gzip Medium
Gzip Text resources 60-80% vs uncompressed Low
Minification JavaScript, CSS 10-30% Low
Tree-shaking JavaScript 20-50% Low
Image compression Images 50-90% Medium-High

Rails Asset Configuration

Setting Purpose Production Value Development Value
config.assets.compile Runtime compilation false true
config.assets.digest Fingerprinting true false
config.assets.compress Compression true false
config.assets.css_compressor CSS minification :sass nil
config.assets.js_compressor JS minification :terser nil