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 |