HTTP 103 Early Hints Guide
This guide shows you how to use HTTP 103 Early Hints with Shakapacker to optimize page load performance.
π Related Documentation: For advanced manual control using
send_pack_early_hintsin controllers before expensive work, see early_hints_manual_api.md.
β οΈ CDN Limitation: Most CDNs (Cloudflare, AWS CloudFront, AWS ALB, etc.) strip HTTP 103 Early Hints before they reach end users. While Shakapacker sends early hints correctly, and your application server (Puma/Thruster) forwards them properly, CDNs typically strip the 103 response. End users will only receive the 200 response with Link headers (which arrive too late to provide early hints benefits). This is an industry-wide limitationβeven major sites like GitHub, Google, and Shopify don't serve 103 in production through CDNs. For full early hints delivery, you need either direct connections without a CDN, or specific CDN configuration (e.g., Cloudflare's Early Hints feature on paid plans, which works differently by caching Link headers). See Reverse proxy stripping 103 responses for configuration details.
What are Early Hints?β
HTTP 103 Early Hints is emitted after Rails has finished rendering but before the final response is sent, allowing browsers to begin fetching resources (JS, CSS) prior to receiving the full HTML response. This may significantly improve page load performance or cause an equally significant regression, depending on the page's content.
β οΈ Critical: Preloading JavaScript may hurt your LCP (Largest Contentful Paint) metric if you have large images, videos, or other content that should load first. Careful experimentation and performance measurement is required.
Priority Levels: Preload vs Prefetch vs Noneβ
Early hints let you control browser download priority for assets:
preload- Prioritize: High priority, browser downloads immediately before HTML parsing. Use for critical assets needed for initial render.prefetch- De-prioritize: Low priority, browser downloads when idle (doesn't compete for bandwidth). Use for non-critical assets or future navigation.none- Default behavior: No hint sent. Browser discovers asset when parsing HTML (normal page load behavior).
Performance Considerationsβ
β οΈ Important: Different pages have different performance characteristics:
- LCP Impact: Preloading JS/CSS competes for bandwidth with images/videos, potentially delaying LCP
- Hero Images: Pages with large hero images usually perform worse with JS/CSS preload
- Interactive Apps: Dashboards and SPAs may benefit from aggressive JS preload
- Content Sites: Blogs and marketing sites often need conservative hints (prefetch or none)
- Recommendation: Configure hints per-page based on content, measure with real user data
Quick Startβ
1. Global Configurationβ
# config/shakapacker.yml
production:
early_hints:
enabled: true # Master switch
css: "preload" # 'preload' | 'prefetch' | 'none'
js: "preload" # 'preload' | 'prefetch' | 'none'
Defaults: When enabled: true, both css and js default to 'preload' if not specified.
Testing: See the Feature Testing Guide for detailed instructions on verifying early hints are working using browser DevTools or curl.
2. Per-Page Configuration (Recommended)β
Configure hints based on your page content:
class PostsController < ApplicationController
# Image-heavy landing page - don't compete with images
configure_pack_early_hints only: [:index], css: 'none', js: 'prefetch'
# Interactive post editor - JS is critical
configure_pack_early_hints only: [:edit], css: 'preload', js: 'preload'
# API endpoints - no hints needed
skip_send_pack_early_hints only: [:api_data]
end
3. Dynamic Configurationβ
Configure based on content:
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
if @post.has_hero_video?
# Video is LCP - don't compete
configure_pack_early_hints all: 'none'
elsif @post.interactive?
# JS needed for interactivity
configure_pack_early_hints css: 'prefetch', js: 'preload'
else
# Standard blog post
configure_pack_early_hints css: 'preload', js: 'prefetch'
end
end
end
4. Preloading Hero Images and Videosβ
Use Rails' built-in preload_link_tag to preload hero images, videos, and other LCP resources. Rails automatically sends these as early hints:
<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
<head>
<%= preload_link_tag image_path('hero.jpg'), as: 'image', type: 'image/jpeg' %>
<%= preload_link_tag video_path('intro.mp4'), as: 'video', type: 'video/mp4' %>
</head>
<body>
<%= yield %>
</body>
</html>
Dynamic preloading in views:
<%# app/views/posts/show.html.erb %>
<% if @post.hero_image_url.present? %>
<%= preload_link_tag @post.hero_image_url, as: 'image' %>
<% end %>
Benefits:
- β Standard Rails API - no custom Shakapacker code needed
- β Automatically sends early hints when server supports it
- β
Works with
image_path,video_path,asset_pathhelpers - β
Supports all standard attributes:
as,type,crossorigin,integrity
When to preload images/videos:
- Hero images that are LCP (Largest Contentful Paint) elements
- Above-the-fold images critical for initial render
- Background videos that play on page load
Performance tip: Don't over-preload! Each preload competes for bandwidth. Focus only on critical resources that improve LCP.
See Rails preload_link_tag docs for full API.
Controller Configurationβ
Skip Early Hints Entirelyβ
class ApiController < ApplicationController
# Skip for entire controller
skip_send_pack_early_hints
end
class PostsController < ApplicationController
# Skip for specific actions
skip_send_pack_early_hints only: [:api_endpoint, :feed]
end
Configure Per Action (Class Method)β
class PostsController < ApplicationController
# Configure specific actions
configure_pack_early_hints only: [:show], css: 'prefetch', js: 'preload'
configure_pack_early_hints only: [:gallery], css: 'none', js: 'none'
# Use 'all' shortcut
configure_pack_early_hints only: [:about], all: 'prefetch'
# Mix general and specific (specific wins)
configure_pack_early_hints only: [:dashboard], all: 'preload', css: 'prefetch'
# Result: css='prefetch', js='preload'
end
Configure in Action Methodβ
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
# Configure based on runtime logic
if @post.video_content?
configure_pack_early_hints css: 'none', js: 'none'
end
end
end
Configure in Before Actionβ
class PostsController < ApplicationController
before_action :optimize_for_images, only: [:gallery, :portfolio]
private
def optimize_for_images
configure_pack_early_hints css: 'prefetch', js: 'prefetch'
end
end
Configuration Precedenceβ
Settings are applied in this order (later overrides earlier):
- Global (shakapacker.yml) - project defaults
- Controller class (configure_pack_early_hints) - per-action defaults
- Manual call (send_pack_early_hints in view) - explicit override
Within a single configuration, all: is applied first, then specific css: and js: values override it.
Usage Examples by Scenarioβ
Scenario 1: Image-Heavy Landing Pageβ
Problem: Large hero image is LCP, JS/CSS hints compete for bandwidth and delay image loading
class HomeController < ApplicationController
def index
# Save bandwidth for hero image
configure_pack_early_hints css: 'none', js: 'prefetch'
end
end
Why:
css: 'none'- No hint sent, CSS discovered normally (saves bandwidth)js: 'prefetch'- Low priority hint, JS downloads when idle (doesn't compete)- Result: Hero image gets full bandwidth priority for better LCP
Scenario 2: Interactive Dashboardβ
Problem: App is useless without JavaScript
class DashboardController < ApplicationController
# JS is critical for all actions
configure_pack_early_hints all: 'preload'
end
Why: Fast JS load is more important than LCP
Scenario 3: Blog with Varied Contentβ
Problem: Article pages have images, index doesn't
class ArticlesController < ApplicationController
# Index: no large images
configure_pack_early_hints only: [:index], css: 'preload', js: 'preload'
# Show: featured images
configure_pack_early_hints only: [:show], css: 'prefetch', js: 'prefetch'
end
Why: Different pages have different performance needs
Scenario 4: Mixed Content Typesβ
Problem: Posts contain videos, images, or interactive content
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
case @post.content_type
when 'video'
# Video is LCP
configure_pack_early_hints all: 'none'
when 'interactive'
# JS needed immediately
configure_pack_early_hints css: 'prefetch', js: 'preload'
when 'image_gallery'
# Images are LCP
configure_pack_early_hints all: 'prefetch'
else
# Standard text post
configure_pack_early_hints css: 'preload', js: 'prefetch'
end
end
end
Why: Dynamic configuration based on actual content
Scenario 5: E-commerce Product Pagesβ
Problem: Product images are critical, but checkout needs JS
class ProductsController < ApplicationController
# Product page: images are critical
configure_pack_early_hints only: [:show], css: 'prefetch', js: 'prefetch'
# Checkout: form validation needs JS
configure_pack_early_hints only: [:checkout], css: 'preload', js: 'preload'
end
Why: Shopping vs checkout have different needs
How It Worksβ
Shakapacker automatically sends early hints after your views render:
1. Request arrives
2. Controller action runs β Database queries, business logic
3. Views render β append_javascript_pack_tag('admin')
4. Layout renders β javascript_pack_tag, stylesheet_pack_tag
5. after_action hook β Reads configuration and queues
6. HTTP 103 sent β rel=preload or rel=prefetch based on config
7. HTTP 200 sent β Full HTML response
Important timing note: HTTP 103 is sent after rendering completes but before the final HTTP 200 response. This means:
- β Benefits: Browser starts downloading assets while the server transmits the final HTML response
- β Limitations: Does NOT help during database queries or view renderingβonly helps with network transfer time
- π‘ Best for: Pages with large HTML responses where asset downloads can happen in parallel with HTML transmission
Advanced: Manual Controlβ
Most apps should use controller configuration. For advanced use cases including:
- Sending hints before expensive controller work for maximum parallelism
- Per-pack customization in layouts
- View-specific logic
See the Manual API Guide for detailed examples and patterns.
Requirementsβ
- Rails 5.2+ (for
request.send_early_hintssupport) - Web server with HTTP/2 and early hints:
- Puma 5+ β
- nginx 1.13+ with ngx_http_v2_module β
- Other HTTP/2 servers with early hints support
- Modern browsers:
- Chrome/Edge/Firefox 103+ β
- Safari 16.4+ β
- Infrastructure that preserves 103 responses:
- Direct connections (no CDN) β
- Cloudflare with Early Hints enabled (paid plans, works via Link header caching) β
- Most CDNs/load balancers β (strip 103 responses - see CDN Limitation above)
If requirements not met, feature gracefully degrades with no errors. The Link headers will still be present in the 200 response, which may provide some browser prefetching benefits even when 103 is stripped.
Quick Referenceβ
Priority levels and when to use each:β
preload(Prioritize): Critical assets on text-heavy pages, SPAs, pages without large imagesprefetch(De-prioritize): Non-critical assets, pages with large LCP images/videos (downloads when idle)none(Default behavior): Image/video-heavy pages, API endpoints, SSR pages (no hint sent)
Testing checklist:β
- Measure LCP with Chrome DevTools Performance tab
- Test on real mobile devices
- A/B test configurations with real user data
- Monitor field data with RUM tools
- Test each page type separately
Troubleshootingβ
For comprehensive testing instructions including browser DevTools and curl methods, see the Feature Testing Guide: HTTP 103 Early Hints.
Debug Modeβ
Enable debug mode to see what early hints are being sent (or why they weren't sent):
# config/shakapacker.yml
production:
early_hints:
enabled: true
debug: true # Outputs debug info as HTML comments
Debug mode adds HTML comments to your page showing:
- Whether hints were sent or skipped
- What pack names were processed
- What Link headers were sent
- HTTP/2 support status
- CDN warning reminder (since most CDNs strip 103 responses)
View page source and look for <!-- Shakapacker Early Hints --> comments.
Early hints not appearing:
- Enable debug mode first to see what's happening
- Check for proxy stripping: If debug shows hints sent but curl/DevTools don't show
HTTP/2 103, your reverse proxy or CDN (Control Plane, Cloudflare, AWS ALB/ELB, nginx) is likely stripping 103 responses. This is the most common cause of "missing" early hints - Check
early_hints: enabled: truein shakapacker.yml - Verify HTTP/2 server (Puma 5+, nginx 1.13+)
- Check Network tab shows "h2" protocol and 103 status
Reverse proxy stripping 103 responses:
If debug mode shows hints are sent but they're not reaching clients, configure your proxy:
- nginx: Add
proxy_pass_header Link;to pass through early hints (nginx 1.13+) - Cloudflare: Enable "Early Hints" in Speed > Optimization (paid plans only)
- AWS ALB/ELB: Not supported - ALBs strip 103 responses. Workaround: skip ALB or use CloudFront
- Control Plane: Appears to strip 103 - Contact their support if you need early hints
See the Feature Testing Guide for detailed proxy configuration examples.
Performance got worse:
- Page likely has large images/videos as LCP
- Try
css: 'prefetch', js: 'prefetch'orall: 'none' - Measure LCP before and after changes
Wrong hints sent:
- Check configuration precedence (global β controller β manual)
- Verify values are strings:
'preload'not:preload - Check for typos (case-sensitive)
Referencesβ
Shakapacker Documentationβ
- Feature Testing Guide: HTTP 103 Early Hints - Detailed testing instructions with browser DevTools and curl