Overview
The critical rendering path represents the sequence of steps browsers execute to convert HTML, CSS, and JavaScript into rendered pixels on screen. Understanding this path enables developers to identify and eliminate bottlenecks that delay initial page rendering. The browser must construct the Document Object Model (DOM) from HTML, build the CSS Object Model (CSSOM) from stylesheets, combine these into a render tree, calculate layout geometry, and finally paint pixels to the screen.
Each step in this pipeline depends on previous steps completing. The browser cannot render content until it constructs both the DOM and CSSOM. Render-blocking resources like external stylesheets and synchronous scripts halt this progression, increasing the time until users see content. Optimizing the critical rendering path focuses on minimizing the time from request initiation to first meaningful paint.
Ruby web applications control the HTML structure, asset organization, and resource delivery that directly impact critical rendering path performance. Rails applications, Sinatra services, and other Ruby web frameworks determine how browsers receive and process resources. A Ruby application serving bloated HTML with multiple render-blocking CSS files creates a slower critical rendering path than one delivering optimized, prioritized resources.
# Basic HTML generation affecting critical rendering path
class PageController < ApplicationController
def index
# Browser cannot render until all CSS loads
@stylesheets = ['reset.css', 'layout.css', 'components.css', 'theme.css']
# Synchronous scripts block parsing
@scripts = ['jquery.js', 'analytics.js', 'app.js']
end
end
The distinction between loading a resource and executing it matters for performance. The browser can download resources in parallel but must process them sequentially for construction steps. A page with ten small CSS files performs differently than one large concatenated file, even at identical total bytes, due to network overhead and parsing behavior.
Key Principles
The critical rendering path consists of five distinct stages that browsers execute in sequence. Each stage transforms data from the previous stage until pixels appear on screen.
DOM Construction occurs when the browser parses HTML markup into a tree structure representing document elements. The parser processes HTML incrementally, building the DOM as bytes arrive. The parser encounters opening tags, creates corresponding nodes, establishes parent-child relationships, and handles closing tags. External resources referenced in HTML (images, stylesheets, scripts) trigger additional requests but don't block DOM construction unless they are parser-blocking resources.
# HTML structure affecting DOM construction speed
def optimized_structure
<<~HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- Critical CSS inlined -->
<style>#{critical_css}</style>
</head>
<body>
<!-- Content appears here without waiting for external CSS -->
<h1>Immediate Content</h1>
<!-- Non-critical CSS loads asynchronously -->
<link rel="stylesheet" href="full.css" media="print" onload="this.media='all'">
</body>
</html>
HTML
end
CSSOM Construction builds a parallel tree structure from CSS rules. Unlike HTML, CSS requires complete parsing before the browser can proceed. The CSSOM must be complete because CSS rules are hierarchical and cascade - the browser cannot determine final styles without processing all rules. A stylesheet loaded at the end of the document still blocks rendering just as much as one in the head.
The browser computes styles by traversing the CSSOM and DOM together, applying rules based on specificity and cascade order. Selectors like div.container > p.text require the browser to match patterns against DOM nodes. Complex selectors increase computation time, though modern browsers optimize this aggressively.
Render Tree Construction combines the DOM and CSSOM to create a tree containing only visible nodes with computed styles. The browser omits nodes with display: none, script tags, meta tags, and other non-visual elements. Each render tree node contains geometric and styling information needed for layout. The render tree represents what will appear on screen, not every DOM node.
# Generating HTML that creates efficient render trees
class LayoutOptimizer
def conditional_content(user)
# Avoid DOM nodes that never render
return '' unless user.premium?
# Generate only visible content server-side
%(<div class="premium-feature">#{premium_content}</div>)
end
def hidden_vs_absent
# Creates DOM node with display:none (still in render tree)
bad = '<div style="display:none">Hidden</div>'
# Better: don't generate the HTML at all
good = user.show_feature? ? '<div>Visible</div>' : ''
good
end
end
Layout (or reflow) calculates the exact position and size of each render tree node. The browser starts at the root and traverses the tree, computing geometric properties based on viewport dimensions and CSS box model rules. Layout is expensive because it affects the entire page - changing one element's size can cascade to many others. The browser outputs a box model where each node has exact coordinates.
Paint converts the layout tree into actual pixels on screen. The browser creates layers for certain elements (position: fixed, transforms, opacity), paints each layer, and composites them in the correct order. Paint operations include drawing text, colors, images, borders, and shadows. The browser paints in a specific order: background, borders, content, outlines.
The critical rendering path blocks on CSS completely but handles JavaScript differently. Scripts with the defer or async attribute don't block parsing. Scripts without these attributes halt DOM construction, execute, then resume parsing. This blocking occurs because scripts might modify the DOM or query computed styles, requiring complete CSSOM availability.
# Script loading strategies in Ruby templates
module ScriptLoader
def self.critical_scripts
# Inline small, essential scripts
<<~JS
<script>
// Critical feature detection
if (!('querySelector' in document)) {
window.location = '/legacy-browser.html';
}
</script>
JS
end
def self.deferred_scripts
# Non-critical scripts don't block rendering
%(<script defer src="/js/analytics.js"></script>)
end
def self.async_scripts
# Independent scripts load without blocking
%(<script async src="/js/social-widgets.js"></script>)
end
end
Performance Considerations
Critical rendering path optimization targets three metrics: minimizing critical resources, reducing critical bytes, and shortening critical path length. Critical resources are files required before first render - typically HTML, critical CSS, and synchronous JavaScript. Each additional critical resource adds network round trips. Critical bytes represent the total file size of critical resources. Critical path length measures the longest dependency chain.
Inlining Critical CSS eliminates a render-blocking request by embedding essential styles directly in HTML. The browser can render above-the-fold content immediately without waiting for external stylesheets. Critical CSS typically includes layout styles, fonts, colors, and visibility rules for initial viewport content. The remaining CSS loads asynchronously.
# Extracting and inlining critical CSS in Rails
class CriticalCssGenerator
def initialize(view_path)
@view_path = view_path
end
def extract_critical
# Use tools like critical or penthouse to analyze page
# Returns CSS needed for above-the-fold rendering
critical_rules = analyze_viewport_css(@view_path)
minify(critical_rules)
end
private
def analyze_viewport_css(path)
# Identify CSS rules affecting initial viewport
# This typically happens at build time, not runtime
CriticalCssExtractor.extract(
path: path,
viewport: { width: 1300, height: 900 }
)
end
def minify(css)
css.gsub(/\s+/, ' ').gsub(/\s*([{}:;,])\s*/, '\1')
end
end
# Usage in views
def page_with_critical_css
critical = CriticalCssGenerator.new('pages/home').extract_critical
<<~HTML
<head>
<style>#{critical}</style>
<link rel="preload" href="/css/full.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/full.css"></noscript>
</head>
HTML
end
Deferring Non-Critical JavaScript prevents scripts from blocking HTML parsing. The defer attribute loads scripts in parallel with parsing and executes them after DOM construction completes but before DOMContentLoaded fires. Multiple deferred scripts execute in order. The async attribute loads scripts in parallel and executes them as soon as downloaded, potentially out of order.
# Managing script loading priorities
class AssetTagHelper
def script_tag(source, critical: false, defer: true)
attributes = []
if critical
# Critical scripts load synchronously (use sparingly)
%(<script src="#{source}"></script>)
elsif defer
# Most application scripts should defer
%(<script src="#{source}" defer></script>)
else
# Independent third-party scripts can be async
%(<script src="#{source}" async></script>)
end
end
end
# In layout template
class ApplicationLayout
def script_tags
[
script_tag('/js/app.js', defer: true),
script_tag('/js/analytics.js', critical: false, defer: false),
script_tag('https://cdn.example.com/widget.js', critical: false, defer: false)
].join("\n")
end
end
Resource Prioritization guides browsers to fetch critical resources first. The <link rel="preload"> hint tells browsers to fetch resources with high priority without blocking rendering. This proves useful for fonts, critical images, and other resources discovered late in HTML parsing.
# Resource hints for optimal loading
class ResourcePreloader
def self.preload_hints
<<~HTML
<!-- Preload critical font files -->
<link rel="preload" href="/fonts/primary.woff2" as="font" type="font/woff2" crossorigin>
<!-- Preload hero image -->
<link rel="preload" href="/images/hero.jpg" as="image">
<!-- Preconnect to third-party domains -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
HTML
end
end
Compression and Minification reduce critical bytes. Gzip or Brotli compression typically reduces text resources by 70-80%. Minification removes whitespace, comments, and renames variables in JavaScript. CSS minification strips whitespace and eliminates redundant rules.
# Asset compression in Rack middleware
class AssetCompressor
def initialize(app)
@app = app
end
def call(env)
status, headers, response = @app.call(env)
if compressible?(headers)
compressed = compress_body(response)
headers['Content-Encoding'] = 'gzip'
headers['Content-Length'] = compressed.bytesize.to_s
return [status, headers, [compressed]]
end
[status, headers, response]
end
private
def compressible?(headers)
content_type = headers['Content-Type']
content_type&.match?(/text|javascript|json|css/)
end
def compress_body(response)
body = response.respond_to?(:body) ? response.body : response.join
Zlib.gzip(body)
end
end
Font Loading Strategies prevent invisible text during font downloads. The font-display CSS property controls rendering behavior. The value swap shows fallback fonts immediately, then swaps to custom fonts when available. The value optional uses custom fonts only if they load quickly.
Server-Side Rendering delivers complete HTML instead of JavaScript frameworks rendering content client-side. Users see content faster because rendering happens on the server rather than after downloading and executing JavaScript. This particularly benefits mobile users with slower devices and connections.
# Server-side rendering reduces critical path length
class ServerRenderedPage < ApplicationController
def show
@data = Product.featured.limit(10)
# Render complete HTML on server
# Browser displays content immediately without waiting for JS
render :show
end
end
# Contrast with client-side rendering approach
class ClientRenderedPage < ApplicationController
def show
# Returns minimal HTML with empty containers
# Browser must download JS, execute it, fetch data, then render
render :client_shell
end
def data
# JavaScript fetches this after page load
render json: Product.featured.limit(10)
end
end
Code Splitting breaks JavaScript into smaller chunks loaded on demand. The initial bundle contains only code needed for first render. Additional features load when users navigate to them. This reduces critical bytes and speeds initial rendering.
Ruby Implementation
Ruby web applications control critical rendering path performance through HTML generation, asset organization, and server configuration. Rails provides several mechanisms for optimizing resource delivery.
Asset Pipeline Configuration manages JavaScript and CSS compilation, minification, and fingerprinting. The asset pipeline concatenates files, reducing HTTP requests. Fingerprinting enables long-term caching while ensuring browsers fetch updated files.
# config/initializers/assets.rb
Rails.application.config.assets.configure do |env|
# Compress assets in production
env.js_compressor = :terser
env.css_compressor = :sass
# Precompile assets at deploy time
env.precompile += %w[
critical.css
application.js
admin.js
]
end
# Separate critical CSS from main bundle
# app/assets/stylesheets/critical.css.scss
// Only above-the-fold styles
.header { /* ... */ }
.hero { /* ... */ }
.nav { /* ... */ }
# app/assets/stylesheets/application.css.scss
// Everything else loads asynchronously
@import 'components/*';
@import 'pages/*';
View Helpers for Optimized Loading generate HTML with appropriate resource hints and loading attributes.
# app/helpers/performance_helper.rb
module PerformanceHelper
def critical_css_tag
if Rails.env.production?
# Inline critical CSS in production
content_tag :style do
Rails.application.assets['critical.css'].to_s.html_safe
end
else
# Link to file in development for easier debugging
stylesheet_link_tag 'critical'
end
end
def deferred_stylesheet_tag(source)
# Load non-critical CSS without blocking render
tag.link(
rel: 'preload',
href: asset_path(source),
as: 'style',
onload: "this.onload=null;this.rel='stylesheet'"
) +
tag.noscript do
stylesheet_link_tag(source)
end
end
def optimized_script_tag(source, **options)
defaults = { defer: true }
javascript_include_tag(source, **defaults.merge(options))
end
end
# Usage in layout
<!DOCTYPE html>
<html>
<head>
<%= critical_css_tag %>
<%= deferred_stylesheet_tag 'application' %>
</head>
<body>
<%= yield %>
<%= optimized_script_tag 'application' %>
</body>
</html>
Conditional Content Generation prevents unnecessary DOM nodes from reaching the browser.
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def show
# Only query data that will be rendered
@featured_products = Product.visible.featured if show_featured_section?
@user_recommendations = current_user&.recommendations&.limit(5)
# Avoid querying data for hidden content
@admin_panel = nil unless current_user&.admin?
end
private
def show_featured_section?
# Server-side logic prevents generating unused HTML
params[:category] == 'home' && !mobile_request?
end
end
# app/views/pages/show.html.erb
<% if @featured_products.present? %>
<section class="featured">
<%= render @featured_products %>
</section>
<% end %>
<!-- Better than: -->
<!-- <section class="featured" style="display:none"> -->
HTTP/2 Server Push proactively sends resources before browsers request them. This eliminates round trips for critical resources discovered during HTML parsing.
# Puma configuration with Early Hints
# config/puma.rb
early_hints true
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_early_hints
private
def set_early_hints
# Server pushes these resources with HTTP 103 Early Hints
response.headers['Link'] = [
'</assets/critical.css>; rel=preload; as=style',
'</assets/application.js>; rel=preload; as=script',
'</fonts/primary.woff2>; rel=preload; as=font; crossorigin'
].join(', ')
end
end
Fragment Caching reduces server processing time, allowing faster response delivery.
# app/views/products/show.html.erb
<% cache @product do %>
<article class="product">
<h1><%= @product.name %></h1>
<% cache [@product, 'description'] do %>
<%= render 'description', product: @product %>
<% end %>
<% cache [@product, 'specifications'] do %>
<%= render 'specifications', product: @product %>
<% end %>
</article>
<% end %>
Content Delivery Network Integration serves assets from edge locations closer to users.
# config/environments/production.rb
Rails.application.configure do
# CDN hosts for asset URLs
config.asset_host = 'https://cdn.example.com'
# CORS headers for fonts and other cross-origin resources
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=31536000',
'Access-Control-Allow-Origin' => '*'
}
end
# Different CDN for images
# app/helpers/image_helper.rb
module ImageHelper
def cdn_image_tag(source, **options)
cdn_url = "https://images.cdn.example.com/#{source}"
image_tag(cdn_url, **options)
end
end
Practical Examples
Example 1: Converting Render-Blocking CSS
A typical application initially loads all CSS synchronously, blocking rendering until complete.
# Initial implementation - blocks rendering
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>My Application</title>
<%= stylesheet_link_tag 'reset' %>
<%= stylesheet_link_tag 'layout' %>
<%= stylesheet_link_tag 'components' %>
<%= stylesheet_link_tag 'pages' %>
<%= stylesheet_link_tag 'theme' %>
</head>
<body>
<%= yield %>
</body>
</html>
This approach delays rendering until all five stylesheets download and parse. Optimizing requires identifying critical styles and loading non-critical styles asynchronously.
# Optimized implementation
# app/helpers/stylesheet_helper.rb
module StylesheetHelper
def inline_critical_styles
content_tag :style do
critical_css_content.html_safe
end
end
def async_stylesheet_link(source)
content_tag :link,
nil,
rel: 'preload',
href: asset_path(source),
as: 'style',
onload: "this.onload=null;this.rel='stylesheet'"
end
private
def critical_css_content
# In production, read pre-extracted critical CSS
# In development, include basic layout styles
if Rails.env.production?
File.read(Rails.root.join('public', 'assets', 'critical.css'))
else
render_critical_dev_styles
end
end
end
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>My Application</title>
<%= inline_critical_styles %>
<%= async_stylesheet_link 'application.css' %>
<noscript>
<%= stylesheet_link_tag 'application' %>
</noscript>
</head>
<body>
<%= yield %>
</body>
</html>
Example 2: Optimizing Font Loading
Web fonts often cause flash of invisible text (FOIT) during load. Optimizing font loading improves perceived performance.
# app/assets/stylesheets/fonts.css.scss
@font-face {
font-family: 'Primary';
src: url('/fonts/primary.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; // Show fallback immediately, swap when loaded
}
@font-face {
font-family: 'Primary';
src: url('/fonts/primary-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
# app/helpers/font_helper.rb
module FontHelper
def preload_fonts
fonts = [
'/fonts/primary.woff2',
'/fonts/primary-bold.woff2'
]
fonts.map do |font|
tag.link(
rel: 'preload',
href: asset_path(font),
as: 'font',
type: 'font/woff2',
crossorigin: 'anonymous'
)
end.join.html_safe
end
end
# app/views/layouts/application.html.erb
<head>
<%= preload_fonts %>
<style>
body {
font-family: 'Primary', -apple-system, BlinkMacSystemFont, sans-serif;
}
</style>
</head>
Example 3: Progressive HTML Streaming
Ruby applications can stream HTML responses, sending content as it generates rather than waiting for complete rendering.
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
# Stream response for faster first byte
self.response_body = Enumerator.new do |stream|
# Send HTML header immediately
stream << render_to_string(
partial: 'layouts/header',
layout: false
)
# Stream products as they load from database
Product.find_each(batch_size: 10) do |batch|
stream << render_to_string(
partial: 'product',
collection: batch,
layout: false
)
end
# Send footer
stream << render_to_string(
partial: 'layouts/footer',
layout: false
)
end
end
end
Example 4: Responsive Image Loading
Serving appropriately sized images reduces critical bytes and speeds rendering.
# app/helpers/responsive_image_helper.rb
module ResponsiveImageHelper
def responsive_image_tag(source, **options)
# Generate multiple sizes
sizes = [400, 800, 1200, 1600]
srcset = sizes.map do |width|
"#{image_url(source, width: width)} #{width}w"
end.join(', ')
image_tag(
image_url(source, width: 800),
srcset: srcset,
sizes: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px',
loading: 'lazy', # Defer off-screen images
**options
)
end
private
def image_url(source, width:)
# Integration with image processing service
"https://images.cdn.example.com/#{source}?w=#{width}&q=85"
end
end
# Usage
<%= responsive_image_tag(
'product.jpg',
alt: 'Product image',
class: 'product-image'
) %>
Tools & Ecosystem
Lighthouse audits web pages for performance, accessibility, and best practices. The tool measures critical rendering path metrics including First Contentful Paint, Largest Contentful Paint, and Time to Interactive. Lighthouse identifies render-blocking resources and provides optimization recommendations.
# Gemfile
gem 'lighthouse-rb' # Ruby wrapper for Lighthouse CLI
# lib/tasks/performance.rake
namespace :performance do
desc 'Run Lighthouse audit on production homepage'
task audit: :environment do
require 'lighthouse'
report = Lighthouse.audit(
url: 'https://example.com',
emulated_form_factor: 'mobile',
throttling_method: 'simulate',
categories: ['performance']
)
puts "Performance Score: #{report.score}"
puts "First Contentful Paint: #{report.metrics.first_contentful_paint}"
puts "Largest Contentful Paint: #{report.metrics.largest_contentful_paint}"
# Identify render-blocking resources
blocking_resources = report.audits['render-blocking-resources']
if blocking_resources.details
puts "\nRender-blocking resources:"
blocking_resources.details.items.each do |item|
puts " #{item.url} - #{item.total_bytes} bytes"
end
end
end
end
WebPageTest provides detailed waterfall charts showing resource loading timelines. The service tests from multiple locations and connection speeds, revealing critical rendering path bottlenecks.
PageSpeed Insights combines Lighthouse metrics with real-user data from Chrome User Experience Report. This reveals how actual users experience a site's performance.
Rails Performance Tools include several gems for optimizing critical rendering path:
# Gemfile
gem 'turbo-rails' # Reduces full-page loads
gem 'propshaft' # Modern asset pipeline
gem 'image_processing' # Optimize uploaded images
gem 'rack-mini-profiler' # Development performance insights
# Critical CSS extraction (build-time tool)
gem 'critical_css_rails', group: :development
Critical CSS Extraction Tools analyze pages to identify above-the-fold styles:
# lib/tasks/critical_css.rake
namespace :assets do
desc 'Extract critical CSS for key pages'
task extract_critical: :environment do
require 'critical_css'
pages = {
'home' => 'http://localhost:3000/',
'products' => 'http://localhost:3000/products',
'checkout' => 'http://localhost:3000/checkout'
}
pages.each do |name, url|
critical_css = CriticalCss.extract(
url: url,
width: 1300,
height: 900,
ignore: [/\.optional/, /@media print/]
)
output_path = Rails.root.join('app', 'assets', 'stylesheets', "critical_#{name}.css")
File.write(output_path, critical_css)
puts "Extracted critical CSS for #{name} (#{critical_css.bytesize} bytes)"
end
end
end
Browser DevTools provide real-time critical rendering path analysis. Chrome DevTools Performance panel shows each rendering stage with precise timing. The Coverage panel identifies unused CSS and JavaScript. Network panel displays resource loading waterfall with blocking indicators.
Content Delivery Networks accelerate asset delivery through edge caching:
# config/initializers/cdn.rb
Rails.application.configure do
if Rails.env.production?
config.action_controller.asset_host = ENV['CDN_HOST']
# Purge CDN cache on deploy
config.after_initialize do
if defined?(Cloudflare)
Cloudflare.configure do |c|
c.api_token = ENV['CLOUDFLARE_TOKEN']
c.zone_id = ENV['CLOUDFLARE_ZONE']
end
end
end
end
end
# Automated cache purging
class CdnCachePurger
def self.purge_assets
client = Cloudflare::Client.new
files = [
'https://cdn.example.com/assets/application.css',
'https://cdn.example.com/assets/application.js'
]
client.purge_files(files)
end
end
Common Pitfalls
Blocking Resources in Head represents the most frequent critical rendering path mistake. Developers place all stylesheets and scripts in the document head, forcing the browser to wait for each resource before rendering.
# Problematic pattern
<head>
<%= stylesheet_link_tag 'reset' %>
<%= stylesheet_link_tag 'layout' %>
<%= stylesheet_link_tag 'components' %>
<%= javascript_include_tag 'jquery' %>
<%= javascript_include_tag 'analytics' %>
<%= javascript_include_tag 'application' %>
</head>
# Solution: Differentiate critical and non-critical resources
<head>
<%= inline_critical_styles %>
<%= async_stylesheet_link 'application' %>
</head>
<body>
<%= yield %>
<%= javascript_include_tag 'application', defer: true %>
<%= javascript_include_tag 'analytics', async: true %>
</body>
Excessive Inline Styles overcorrects by inlining too much CSS. Inlining hundreds of kilobytes of styles eliminates caching benefits and increases HTML size. Critical CSS should only include styles for above-the-fold content, typically 10-20KB.
# Problematic: Inlining entire stylesheet
<head>
<style>
<%= Rails.application.assets['application.css'].to_s %>
</style>
</head>
# Correct: Only inline critical subset
<head>
<style>
<%= Rails.application.assets['critical.css'].to_s %>
</style>
<%= async_stylesheet_link 'application' %>
</head>
Font Loading Without Optimization causes flash of invisible text or layout shifts. Browsers hide text during font downloads by default (FOIT behavior). Specifying font-display: swap shows text immediately with fallback fonts.
Unused CSS and JavaScript increases critical bytes unnecessarily. Many applications include entire frameworks when using minimal features. Tree-shaking and code splitting remove unused code.
# Remove unused framework features
# Before: Including entire Bootstrap
gem 'bootstrap', '~> 5.0'
# After: Import only needed components
# app/assets/stylesheets/application.scss
@import 'bootstrap/functions';
@import 'bootstrap/variables';
@import 'bootstrap/grid';
@import 'bootstrap/utilities';
// Exclude unused components like carousel, modals, etc.
Third-Party Script Blocking occurs when external scripts load synchronously. Analytics, advertisements, and social widgets block rendering when loaded without async or defer attributes.
# Problematic: Synchronous third-party scripts
<head>
<script src="https://analytics.example.com/track.js"></script>
<script src="https://ads.example.com/serve.js"></script>
</head>
# Solution: Async loading for third-party content
<head>
<script async src="https://analytics.example.com/track.js"></script>
<script async src="https://ads.example.com/serve.js"></script>
</head>
Image-Heavy Above-Fold Content delays rendering when large images load synchronously. Hero images and banners increase critical path length without optimization.
# Use responsive images with appropriate sizing
module ImageOptimizer
def optimized_hero(source)
image_tag(
cdn_image_url(source, width: 1200, quality: 85),
srcset: hero_srcset(source),
sizes: '100vw',
fetchpriority: 'high', # Prioritize hero image
alt: 'Hero image'
)
end
private
def hero_srcset(source)
[400, 800, 1200, 1600].map do |w|
"#{cdn_image_url(source, width: w, quality: 85)} #{w}w"
end.join(', ')
end
end
Deeply Nested DOM Structures increase layout calculation time. Browsers traverse the DOM tree to compute layout, and deeply nested structures with many children slow this process.
Excessive Reflows occur when JavaScript repeatedly modifies layout properties. Reading layout properties (offsetHeight, clientWidth) forces synchronous layout calculation. Batching DOM modifications reduces reflow count.
Reference
Critical Rendering Path Stages
| Stage | Input | Output | Blocks Rendering |
|---|---|---|---|
| DOM Construction | HTML bytes | Document Object Model | No (incremental) |
| CSSOM Construction | CSS bytes | CSS Object Model | Yes (complete parse required) |
| Render Tree | DOM + CSSOM | Visible nodes with styles | Yes |
| Layout | Render tree | Geometry of each node | Yes |
| Paint | Layout tree | Pixels on screen | Yes |
Resource Loading Attributes
| Attribute | Loading | Execution | Parse Blocking | Use Case |
|---|---|---|---|---|
| None (default) | Immediately | When loaded | Yes | Critical synchronous scripts |
| async | Parallel with parsing | As soon as loaded | No | Independent third-party scripts |
| defer | Parallel with parsing | After DOM ready | No | Application scripts needing full DOM |
| preload | High priority parallel | Manual | No | Critical resources discovered late |
| prefetch | Low priority parallel | Manual | No | Resources for next navigation |
Font Display Values
| Value | Behavior | Block Period | Swap Period | Use When |
|---|---|---|---|---|
| auto | Browser default | Short (~100ms) | Infinite | Browser decides best approach |
| block | Hide text | Long (~3s) | Infinite | Font critical to design |
| swap | Show fallback immediately | None | Infinite | Readability over design consistency |
| fallback | Hide briefly | Short (~100ms) | Short (~3s) | Balance readability and design |
| optional | Hide briefly | Short (~100ms) | None | Font only if cached |
Performance Metrics
| Metric | Measures | Target | Critical Path Impact |
|---|---|---|---|
| First Contentful Paint (FCP) | First text/image render | < 1.8s | Direct - measures critical path completion |
| Largest Contentful Paint (LCP) | Largest content render | < 2.5s | Indirect - affected by critical resources |
| Time to Interactive (TTI) | Page fully interactive | < 3.8s | Direct - requires JS parsing completion |
| Total Blocking Time (TBT) | Main thread blocking time | < 200ms | Direct - long tasks delay interactivity |
| Cumulative Layout Shift (CLS) | Visual stability | < 0.1 | Indirect - affected by font/image loading |
Rails Helper Methods
| Method | Purpose | Example Output |
|---|---|---|
| stylesheet_link_tag | Generate style link | link rel=stylesheet |
| javascript_include_tag | Generate script tag | script src=path |
| image_tag | Generate img element | img src=path alt=text |
| asset_path | Generate asset URL | /assets/file-fingerprint.ext |
| content_tag | Generate HTML tag | Custom element with content |
Critical CSS Size Guidelines
| Content Type | Typical Critical CSS Size | Notes |
|---|---|---|
| Marketing page | 8-12 KB | Simple layout, hero section |
| E-commerce listing | 12-18 KB | Product grids, filters |
| Application dashboard | 15-25 KB | Complex layouts, navigation |
| Blog post | 5-10 KB | Minimal chrome, content focus |
| Maximum recommended | 14 KB | Fits in TCP initial congestion window |
HTTP/2 Optimization Patterns
| Pattern | Benefit | Implementation |
|---|---|---|
| Server Push | Eliminates round trips | Early Hints header with resource links |
| Multiplexing | Parallel resource loading | Multiple files over single connection |
| Header Compression | Reduces overhead | HPACK compression automatic |
| Prioritization | Critical resources first | Stream priority in HTTP/2 frames |
| Binary Protocol | Faster parsing | HTTP/2 handles automatically |