shakapacker 9.3.0.beta.2 → 9.3.0.beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,440 @@
1
+ # HTTP 103 Early Hints Guide
2
+
3
+ This guide shows you how to use HTTP 103 Early Hints with Shakapacker to optimize page load performance.
4
+
5
+ ## What are Early Hints?
6
+
7
+ 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.
8
+
9
+ ⚠️ **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.**
10
+
11
+ ### Priority Levels: Preload vs Prefetch vs None
12
+
13
+ Early hints let you control browser download priority for assets:
14
+
15
+ - **`preload`** - **Prioritize**: High priority, browser downloads immediately before HTML parsing. Use for critical assets needed for initial render.
16
+ - **`prefetch`** - **De-prioritize**: Low priority, browser downloads when idle (doesn't compete for bandwidth). Use for non-critical assets or future navigation.
17
+ - **`none`** - **Default behavior**: No hint sent. Browser discovers asset when parsing HTML (normal page load behavior).
18
+
19
+ ### Performance Considerations
20
+
21
+ ⚠️ **Important**: Different pages have different performance characteristics:
22
+
23
+ - **LCP Impact**: Preloading JS/CSS competes for bandwidth with images/videos, potentially delaying LCP
24
+ - **Hero Images**: Pages with large hero images usually perform **worse** with JS/CSS preload
25
+ - **Interactive Apps**: Dashboards and SPAs may benefit from aggressive JS preload
26
+ - **Content Sites**: Blogs and marketing sites often need conservative hints (prefetch or none)
27
+ - **Recommendation**: Configure hints **per-page** based on content, measure with real user data
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Global Configuration
32
+
33
+ ```yaml
34
+ # config/shakapacker.yml
35
+ production:
36
+ early_hints:
37
+ enabled: true # Master switch
38
+ css: "preload" # 'preload' | 'prefetch' | 'none'
39
+ js: "preload" # 'preload' | 'prefetch' | 'none'
40
+ ```
41
+
42
+ **Defaults**: When `enabled: true`, both `css` and `js` default to `'preload'` if not specified.
43
+
44
+ **Testing**: See the [Feature Testing Guide](feature_testing.md#http-103-early-hints) for detailed instructions on verifying early hints are working using browser DevTools or curl.
45
+
46
+ ### 2. Per-Page Configuration (Recommended)
47
+
48
+ Configure hints based on your page content:
49
+
50
+ ```ruby
51
+ class PostsController < ApplicationController
52
+ # Image-heavy landing page - don't compete with images
53
+ configure_pack_early_hints only: [:index], css: 'none', js: 'prefetch'
54
+
55
+ # Interactive post editor - JS is critical
56
+ configure_pack_early_hints only: [:edit], css: 'preload', js: 'preload'
57
+
58
+ # API endpoints - no hints needed
59
+ skip_send_pack_early_hints only: [:api_data]
60
+ end
61
+ ```
62
+
63
+ ### 3. Dynamic Configuration
64
+
65
+ Configure based on content:
66
+
67
+ ```ruby
68
+ class PostsController < ApplicationController
69
+ def show
70
+ @post = Post.find(params[:id])
71
+
72
+ if @post.has_hero_video?
73
+ # Video is LCP - don't compete
74
+ configure_pack_early_hints all: 'none'
75
+ elsif @post.interactive?
76
+ # JS needed for interactivity
77
+ configure_pack_early_hints css: 'prefetch', js: 'preload'
78
+ else
79
+ # Standard blog post
80
+ configure_pack_early_hints css: 'preload', js: 'prefetch'
81
+ end
82
+ end
83
+ end
84
+ ```
85
+
86
+ ### 4. Preloading Hero Images and Videos
87
+
88
+ Use Rails' built-in `preload_link_tag` to preload hero images, videos, and other LCP resources. Rails automatically sends these as early hints:
89
+
90
+ ```erb
91
+ <%# app/views/layouts/application.html.erb %>
92
+ <!DOCTYPE html>
93
+ <html>
94
+ <head>
95
+ <%= preload_link_tag image_path('hero.jpg'), as: 'image', type: 'image/jpeg' %>
96
+ <%= preload_link_tag video_path('intro.mp4'), as: 'video', type: 'video/mp4' %>
97
+ </head>
98
+ <body>
99
+ <%= yield %>
100
+ </body>
101
+ </html>
102
+ ```
103
+
104
+ **Dynamic preloading in views:**
105
+
106
+ ```erb
107
+ <%# app/views/posts/show.html.erb %>
108
+ <% if @post.hero_image_url.present? %>
109
+ <%= preload_link_tag @post.hero_image_url, as: 'image' %>
110
+ <% end %>
111
+ ```
112
+
113
+ **Benefits:**
114
+
115
+ - ✅ Standard Rails API - no custom Shakapacker code needed
116
+ - ✅ Automatically sends early hints when server supports it
117
+ - ✅ Works with `image_path`, `video_path`, `asset_path` helpers
118
+ - ✅ Supports all standard attributes: `as`, `type`, `crossorigin`, `integrity`
119
+
120
+ **When to preload images/videos:**
121
+
122
+ - Hero images that are LCP (Largest Contentful Paint) elements
123
+ - Above-the-fold images critical for initial render
124
+ - Background videos that play on page load
125
+
126
+ **Performance tip:** Don't over-preload! Each preload competes for bandwidth. Focus only on critical resources that improve LCP.
127
+
128
+ See [Rails preload_link_tag docs](https://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html#method-i-preload_link_tag) for full API.
129
+
130
+ ## Controller Configuration
131
+
132
+ #### Skip Early Hints Entirely
133
+
134
+ ```ruby
135
+ class ApiController < ApplicationController
136
+ # Skip for entire controller
137
+ skip_send_pack_early_hints
138
+ end
139
+
140
+ class PostsController < ApplicationController
141
+ # Skip for specific actions
142
+ skip_send_pack_early_hints only: [:api_endpoint, :feed]
143
+ end
144
+ ```
145
+
146
+ #### Configure Per Action (Class Method)
147
+
148
+ ```ruby
149
+ class PostsController < ApplicationController
150
+ # Configure specific actions
151
+ configure_pack_early_hints only: [:show], css: 'prefetch', js: 'preload'
152
+ configure_pack_early_hints only: [:gallery], css: 'none', js: 'none'
153
+
154
+ # Use 'all' shortcut
155
+ configure_pack_early_hints only: [:about], all: 'prefetch'
156
+
157
+ # Mix general and specific (specific wins)
158
+ configure_pack_early_hints only: [:dashboard], all: 'preload', css: 'prefetch'
159
+ # Result: css='prefetch', js='preload'
160
+ end
161
+ ```
162
+
163
+ #### Configure in Action Method
164
+
165
+ ```ruby
166
+ class PostsController < ApplicationController
167
+ def show
168
+ @post = Post.find(params[:id])
169
+
170
+ # Configure based on runtime logic
171
+ if @post.video_content?
172
+ configure_pack_early_hints css: 'none', js: 'none'
173
+ end
174
+ end
175
+ end
176
+ ```
177
+
178
+ #### Configure in Before Action
179
+
180
+ ```ruby
181
+ class PostsController < ApplicationController
182
+ before_action :optimize_for_images, only: [:gallery, :portfolio]
183
+
184
+ private
185
+
186
+ def optimize_for_images
187
+ configure_pack_early_hints css: 'prefetch', js: 'prefetch'
188
+ end
189
+ end
190
+ ```
191
+
192
+ ## Configuration Precedence
193
+
194
+ Settings are applied in this order (later overrides earlier):
195
+
196
+ 1. **Global** (shakapacker.yml) - project defaults
197
+ 2. **Controller class** (configure_pack_early_hints) - per-action defaults
198
+ 3. **Manual call** (send_pack_early_hints in view) - explicit override
199
+
200
+ Within a single configuration, `all:` is applied first, then specific `css:` and `js:` values override it.
201
+
202
+ ## Usage Examples by Scenario
203
+
204
+ ### Scenario 1: Image-Heavy Landing Page
205
+
206
+ **Problem**: Large hero image is LCP, JS/CSS hints compete for bandwidth and delay image loading
207
+
208
+ ```ruby
209
+ class HomeController < ApplicationController
210
+ def index
211
+ # Save bandwidth for hero image
212
+ configure_pack_early_hints css: 'none', js: 'prefetch'
213
+ end
214
+ end
215
+ ```
216
+
217
+ **Why**:
218
+
219
+ - `css: 'none'` - No hint sent, CSS discovered normally (saves bandwidth)
220
+ - `js: 'prefetch'` - Low priority hint, JS downloads when idle (doesn't compete)
221
+ - **Result**: Hero image gets full bandwidth priority for better LCP
222
+
223
+ ### Scenario 2: Interactive Dashboard
224
+
225
+ **Problem**: App is useless without JavaScript
226
+
227
+ ```ruby
228
+ class DashboardController < ApplicationController
229
+ # JS is critical for all actions
230
+ configure_pack_early_hints all: 'preload'
231
+ end
232
+ ```
233
+
234
+ **Why**: Fast JS load is more important than LCP
235
+
236
+ ### Scenario 3: Blog with Varied Content
237
+
238
+ **Problem**: Article pages have images, index doesn't
239
+
240
+ ```ruby
241
+ class ArticlesController < ApplicationController
242
+ # Index: no large images
243
+ configure_pack_early_hints only: [:index], css: 'preload', js: 'preload'
244
+
245
+ # Show: featured images
246
+ configure_pack_early_hints only: [:show], css: 'prefetch', js: 'prefetch'
247
+ end
248
+ ```
249
+
250
+ **Why**: Different pages have different performance needs
251
+
252
+ ### Scenario 4: Mixed Content Types
253
+
254
+ **Problem**: Posts contain videos, images, or interactive content
255
+
256
+ ```ruby
257
+ class PostsController < ApplicationController
258
+ def show
259
+ @post = Post.find(params[:id])
260
+
261
+ case @post.content_type
262
+ when 'video'
263
+ # Video is LCP
264
+ configure_pack_early_hints all: 'none'
265
+ when 'interactive'
266
+ # JS needed immediately
267
+ configure_pack_early_hints css: 'prefetch', js: 'preload'
268
+ when 'image_gallery'
269
+ # Images are LCP
270
+ configure_pack_early_hints all: 'prefetch'
271
+ else
272
+ # Standard text post
273
+ configure_pack_early_hints css: 'preload', js: 'prefetch'
274
+ end
275
+ end
276
+ end
277
+ ```
278
+
279
+ **Why**: Dynamic configuration based on actual content
280
+
281
+ ### Scenario 5: E-commerce Product Pages
282
+
283
+ **Problem**: Product images are critical, but checkout needs JS
284
+
285
+ ```ruby
286
+ class ProductsController < ApplicationController
287
+ # Product page: images are critical
288
+ configure_pack_early_hints only: [:show], css: 'prefetch', js: 'prefetch'
289
+
290
+ # Checkout: form validation needs JS
291
+ configure_pack_early_hints only: [:checkout], css: 'preload', js: 'preload'
292
+ end
293
+ ```
294
+
295
+ **Why**: Shopping vs checkout have different needs
296
+
297
+ ## How It Works
298
+
299
+ Shakapacker automatically sends early hints after your views render:
300
+
301
+ ```text
302
+ 1. Request arrives
303
+ 2. Controller action runs → Database queries, business logic
304
+ 3. Views render → append_javascript_pack_tag('admin')
305
+ 4. Layout renders → javascript_pack_tag, stylesheet_pack_tag
306
+ 5. after_action hook → Reads configuration and queues
307
+ 6. HTTP 103 sent → rel=preload or rel=prefetch based on config
308
+ 7. HTTP 200 sent → Full HTML response
309
+ ```
310
+
311
+ **Important timing note**: HTTP 103 is sent after rendering completes but before the final HTTP 200 response. This means:
312
+
313
+ - ✅ **Benefits**: Browser starts downloading assets while the server transmits the final HTML response
314
+ - ❌ **Limitations**: Does NOT help during database queries or view rendering—only helps with network transfer time
315
+ - 💡 **Best for**: Pages with large HTML responses where asset downloads can happen in parallel with HTML transmission
316
+
317
+ ## Advanced: Manual Control
318
+
319
+ Most apps should use controller configuration. Use manual control for view-specific logic:
320
+
321
+ ```erb
322
+ <%# app/views/layouts/application.html.erb %>
323
+ <% send_pack_early_hints css: 'prefetch', js: 'preload' %>
324
+
325
+ <!DOCTYPE html>
326
+ <html>
327
+ <body>
328
+ <%= yield %>
329
+ <%= javascript_pack_tag 'application' %>
330
+ </body>
331
+ </html>
332
+ ```
333
+
334
+ **Per-tag override:**
335
+
336
+ ```erb
337
+ <%= javascript_pack_tag 'application',
338
+ early_hints: { css: 'preload', js: 'prefetch' } %>
339
+ ```
340
+
341
+ **Use cases:** Layout-specific optimizations, conditional hints based on view variables, A/B testing
342
+
343
+ ## Requirements
344
+
345
+ - **Rails 5.2+** (for `request.send_early_hints` support)
346
+ - **Web server with HTTP/2 and early hints:**
347
+ - Puma 5+ ✅
348
+ - nginx 1.13+ with ngx_http_v2_module ✅
349
+ - Other HTTP/2 servers with early hints support
350
+ - **Modern browsers:**
351
+ - Chrome/Edge/Firefox 103+ ✅
352
+ - Safari 16.4+ ✅
353
+
354
+ If requirements not met, feature gracefully degrades with no errors.
355
+
356
+ ## Quick Reference
357
+
358
+ ### Priority levels and when to use each:
359
+
360
+ - **`preload`** (Prioritize): Critical assets on text-heavy pages, SPAs, pages without large images
361
+ - **`prefetch`** (De-prioritize): Non-critical assets, pages with large LCP images/videos (downloads when idle)
362
+ - **`none`** (Default behavior): Image/video-heavy pages, API endpoints, SSR pages (no hint sent)
363
+
364
+ ### Testing checklist:
365
+
366
+ 1. Measure LCP with Chrome DevTools Performance tab
367
+ 2. Test on real mobile devices
368
+ 3. A/B test configurations with real user data
369
+ 4. Monitor field data with RUM tools
370
+ 5. Test each page type separately
371
+
372
+ ## Troubleshooting
373
+
374
+ For comprehensive testing instructions including browser DevTools and curl methods, see the [Feature Testing Guide: HTTP 103 Early Hints](feature_testing.md#http-103-early-hints).
375
+
376
+ ### Debug Mode
377
+
378
+ Enable debug mode to see what early hints are being sent (or why they weren't sent):
379
+
380
+ ```yaml
381
+ # config/shakapacker.yml
382
+ production:
383
+ early_hints:
384
+ enabled: true
385
+ debug: true # Outputs debug info as HTML comments
386
+ ```
387
+
388
+ Debug mode adds HTML comments to your page showing:
389
+
390
+ - Whether hints were sent or skipped
391
+ - What pack names were processed
392
+ - What Link headers were sent
393
+ - HTTP/2 support status
394
+
395
+ View page source and look for `<!-- Shakapacker Early Hints Debug -->` comments.
396
+
397
+ **Early hints not appearing:**
398
+
399
+ - **Enable debug mode first** to see what's happening
400
+ - **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
401
+ - Check `early_hints: enabled: true` in shakapacker.yml
402
+ - Verify HTTP/2 server (Puma 5+, nginx 1.13+)
403
+ - Check Network tab shows "h2" protocol and 103 status
404
+
405
+ **Reverse proxy stripping 103 responses:**
406
+
407
+ If debug mode shows hints are sent but they're not reaching clients, configure your proxy:
408
+
409
+ - **nginx**: Add `proxy_pass_header Link;` to pass through early hints (nginx 1.13+)
410
+ - **Cloudflare**: Enable "Early Hints" in Speed > Optimization (paid plans only)
411
+ - **AWS ALB/ELB**: Not supported - ALBs strip 103 responses. Workaround: skip ALB or use CloudFront
412
+ - **Control Plane**: Appears to strip 103 - Contact their support if you need early hints
413
+
414
+ See the [Feature Testing Guide](feature_testing.md#troubleshooting-early-hints) for detailed proxy configuration examples.
415
+
416
+ **Performance got worse:**
417
+
418
+ - Page likely has large images/videos as LCP
419
+ - Try `css: 'prefetch', js: 'prefetch'` or `all: 'none'`
420
+ - Measure LCP before and after changes
421
+
422
+ **Wrong hints sent:**
423
+
424
+ - Check configuration precedence (global → controller → manual)
425
+ - Verify values are strings: `'preload'` not `:preload`
426
+ - Check for typos (case-sensitive)
427
+
428
+ ## References
429
+
430
+ ### Shakapacker Documentation
431
+
432
+ - [Feature Testing Guide: HTTP 103 Early Hints](feature_testing.md#http-103-early-hints) - Detailed testing instructions with browser DevTools and curl
433
+
434
+ ### External Resources
435
+
436
+ - [Rails API: send_early_hints](https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-send_early_hints)
437
+ - [RFC 8297: HTTP Early Hints](https://datatracker.ietf.org/doc/html/rfc8297)
438
+ - [MDN: rel=preload vs rel=prefetch](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel)
439
+ - [Web.dev: Optimize LCP](https://web.dev/optimize-lcp/)
440
+ - [HTTP 103 Explained](https://http.dev/103)