shakapacker 9.3.0.beta.4 → 9.3.0.beta.6

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,454 @@
1
+ # HTTP 103 Early Hints - Manual API Guide
2
+
3
+ > **📚 Main Documentation:** This guide covers the manual `send_pack_early_hints` API for advanced use cases. For the recommended controller-based API (`configure_pack_early_hints`, `skip_send_pack_early_hints`) and comprehensive setup instructions, see [early_hints.md](early_hints.md).
4
+
5
+ This guide focuses on **manual control** of early hints for advanced scenarios where you need to send hints before expensive controller work or customize hints per-pack in layouts.
6
+
7
+ ## Automatic vs Manual API
8
+
9
+ By default, Shakapacker automatically sends early hints when `javascript_pack_tag` and `stylesheet_pack_tag` are called (after views render). The manual API allows you to:
10
+
11
+ - **Send hints earlier** - Before controller work starts, maximizing parallelism
12
+ - **Customize per-pack** - Different strategies for different packs in the same layout
13
+ - **Override automatic behavior** - When you need fine-grained control
14
+
15
+ ## ⚠️ IMPORTANT: Performance Testing Required
16
+
17
+ **Early hints can improve OR hurt performance** depending on your application:
18
+
19
+ - ✅ **May help**: Large JS bundles (>500KB), slow controllers (>300ms), fast CDN
20
+ - ❌ **May hurt**: Large images as LCP (Largest Contentful Paint), small JS bundles
21
+ - ⚠️ **Test before deploying**: Measure LCP and Time to Interactive before/after enabling
22
+
23
+ **How to test:**
24
+
25
+ 1. Enable early hints in production for 10% of traffic
26
+ 2. Measure Core Web Vitals (LCP, FCP, TTI) for both groups
27
+ 3. Only keep enabled if metrics improve
28
+
29
+ See the [Feature Testing Guide](feature_testing.md#http-103-early-hints) for testing instructions and the [main documentation](early_hints.md) for comprehensive troubleshooting.
30
+
31
+ ## When to Use the Manual API
32
+
33
+ Use `send_pack_early_hints` when you need:
34
+
35
+ 1. **Maximum parallelism** - Send hints BEFORE expensive controller work (database queries, API calls)
36
+ 2. **Per-pack customization** - Different hint strategies for different packs in layouts
37
+ 3. **Dynamic control** - Runtime decisions about which packs to hint
38
+
39
+ For most applications, use the [controller-based API](early_hints.md#controller-configuration) instead (`configure_pack_early_hints`, `skip_send_pack_early_hints`).
40
+
41
+ ## Manual API Patterns
42
+
43
+ ### Pattern 1: Per-Pack Customization in Layout
44
+
45
+ Customize hint handling per pack using a hash:
46
+
47
+ ```erb
48
+ <%# app/views/layouts/application.html.erb %>
49
+ <!DOCTYPE html>
50
+ <html>
51
+ <head>
52
+ <%# Mixed handling: preload application, prefetch vendor %>
53
+ <%= stylesheet_pack_tag 'application', 'vendor',
54
+ early_hints: { 'application' => 'preload', 'vendor' => 'prefetch' } %>
55
+ </head>
56
+ <body>
57
+ <%= yield %>
58
+ <%# Disable early hints for this tag %>
59
+ <%= javascript_pack_tag 'application', early_hints: false %>
60
+ </body>
61
+ </html>
62
+ ```
63
+
64
+ **Options:**
65
+
66
+ - `"preload"` - High priority (default)
67
+ - `"prefetch"` - Low priority
68
+ - `false` or `"none"` - Disabled
69
+
70
+ ---
71
+
72
+ ### Pattern 2: Controller Override (Before Expensive Work)
73
+
74
+ Send hints manually in controller BEFORE expensive work to maximize parallelism:
75
+
76
+ ```ruby
77
+ class PostsController < ApplicationController
78
+ def show
79
+ # Send hints BEFORE expensive work
80
+ send_pack_early_hints({
81
+ "application" => { js: "preload", css: "preload" },
82
+ "admin" => { js: "prefetch", css: "none" }
83
+ })
84
+
85
+ # Browser now downloading assets while we do expensive work
86
+ @post = Post.includes(:comments, :author, :tags).find(params[:id])
87
+ @related = @post.find_related_posts(limit: 10) # Expensive query
88
+ # ... more work ...
89
+ end
90
+ end
91
+ ```
92
+
93
+ **Timeline:**
94
+
95
+ 1. Request arrives
96
+ 2. `send_pack_early_hints` called → HTTP 103 sent immediately
97
+ 3. Browser starts downloading assets
98
+ 4. Rails continues with expensive queries (IN PARALLEL with browser downloads)
99
+ 5. View renders
100
+ 6. HTTP 200 sent with full HTML
101
+ 7. Assets already downloaded = faster page load
102
+
103
+ **Benefits:**
104
+
105
+ - ✅ Parallelizes browser downloads with server processing
106
+ - ✅ Can save 200-500ms on pages with slow controllers
107
+ - ✅ Most valuable for pages with expensive queries/API calls
108
+
109
+ ---
110
+
111
+ ### Pattern 3: View Override
112
+
113
+ Views can use `append_*_pack_tag` to add packs dynamically:
114
+
115
+ ```erb
116
+ <%# app/views/posts/edit.html.erb %>
117
+ <% append_javascript_pack_tag 'admin_tools' %>
118
+
119
+ <div class="post-editor">
120
+ <%# ... editor UI ... %>
121
+ </div>
122
+ ```
123
+
124
+ ```erb
125
+ <%# app/views/layouts/application.html.erb %>
126
+ <!DOCTYPE html>
127
+ <html>
128
+ <head>
129
+ <%= stylesheet_pack_tag 'application' %>
130
+ </head>
131
+ <body>
132
+ <%= yield %> <%# View has run, admin_tools added to queue %>
133
+
134
+ <%# Sends hints for BOTH application + admin_tools %>
135
+ <%= javascript_pack_tag 'application' %>
136
+ </body>
137
+ </html>
138
+ ```
139
+
140
+ **How it works:**
141
+
142
+ - Views call `append_javascript_pack_tag('admin_tools')`
143
+ - Layout calls `javascript_pack_tag('application')`
144
+ - Helper combines: `['application', 'admin_tools']`
145
+ - Sends hints for ALL packs automatically
146
+
147
+ ---
148
+
149
+ ## Configuration
150
+
151
+ > **📚 Configuration:** See the [main documentation](early_hints.md#quick-start) for all configuration options including global settings, priority levels (preload/prefetch/none), and per-controller configuration.
152
+
153
+ ---
154
+
155
+ ## Duplicate Prevention
156
+
157
+ Hints are automatically prevented from being sent twice:
158
+
159
+ ```ruby
160
+ # Controller
161
+ def show
162
+ send_pack_early_hints({ "application" => { js: "preload", css: "preload" } })
163
+ # ... expensive work ...
164
+ end
165
+ ```
166
+
167
+ ```erb
168
+ <%# Layout %>
169
+ <%= javascript_pack_tag 'application' %>
170
+ <%# Won't send duplicate hint - already sent in controller %>
171
+ ```
172
+
173
+ **How it works:**
174
+
175
+ - Tracks which packs have sent JS hints: `@early_hints_javascript = {}`
176
+ - Tracks which packs have sent CSS hints: `@early_hints_stylesheets = {}`
177
+ - Skips sending hints for packs already sent
178
+
179
+ ---
180
+
181
+ ## When to Use Each Pattern
182
+
183
+ ### Pattern 1 (Per-Pack) - Best for:
184
+
185
+ - Mixed vendor bundles (preload critical, prefetch non-critical)
186
+ - Different handling for different packs
187
+ - Layout-specific optimizations
188
+
189
+ ### Pattern 2 (Controller) - Best for:
190
+
191
+ - Slow controllers with expensive queries (>300ms)
192
+ - Large JS bundles (>500KB)
193
+ - APIs calls in controller
194
+ - Maximum parallelism needed
195
+
196
+ ### Pattern 3 (View Override) - Best for:
197
+
198
+ - Admin sections with extra packs
199
+ - Feature flags determining packs
200
+ - Page-specific bundles
201
+
202
+ ---
203
+
204
+ ## Full Example: Mixed Patterns
205
+
206
+ ```ruby
207
+ # app/controllers/posts_controller.rb
208
+ class PostsController < ApplicationController
209
+ def index
210
+ # Fast controller, automatic hints work fine (Pattern 1)
211
+ end
212
+
213
+ def show
214
+ # Slow controller, send hints early for parallelism (Pattern 2)
215
+ send_pack_early_hints({
216
+ "application" => { js: "preload", css: "preload" }
217
+ })
218
+
219
+ # Expensive work happens in parallel with browser downloads
220
+ @post = Post.includes(:comments, :author).find(params[:id])
221
+ end
222
+ end
223
+ ```
224
+
225
+ ```erb
226
+ <%# app/views/posts/show.html.erb %>
227
+ <% if current_user&.admin? %>
228
+ <%# Pattern 3: Dynamic pack loading based on user role %>
229
+ <% append_javascript_pack_tag 'admin_tools' %>
230
+ <% end %>
231
+ ```
232
+
233
+ ```erb
234
+ <%# app/views/layouts/application.html.erb %>
235
+ <!DOCTYPE html>
236
+ <html>
237
+ <head>
238
+ <%= stylesheet_pack_tag 'application' %>
239
+ </head>
240
+ <body>
241
+ <%= yield %>
242
+
243
+ <%# Sends hints for application + admin_tools (if appended) %>
244
+ <%# Won't duplicate hints already sent in controller %>
245
+ <%= javascript_pack_tag 'application' %>
246
+ </body>
247
+ </html>
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Preloading Non-Pack Assets (Images, Videos, Fonts)
253
+
254
+ **Shakapacker's early hints are for pack assets (JS/CSS bundles).** For non-pack assets like hero images, videos, and fonts, you have two options.
255
+
256
+ > **Note:** The [main documentation](early_hints.md#4-preloading-hero-images-and-videos) covers using Rails' built-in `preload_link_tag` for images and videos, which is simpler than the manual approach below.
257
+
258
+ ### Option 1: Manual Early Hints (For LCP/Critical Assets)
259
+
260
+ **IMPORTANT:** Browsers only process the FIRST HTTP 103 response. If you need both pack assets AND images/videos in early hints, you must send them together in ONE call.
261
+
262
+ ```ruby
263
+ class PostsController < ApplicationController
264
+ before_action :send_critical_early_hints, only: [:show]
265
+
266
+ private
267
+
268
+ def send_critical_early_hints
269
+ # Build all early hints in ONE call (packs + images)
270
+ links = []
271
+
272
+ # Pack assets (using Shakapacker manifest)
273
+ js_path = "/packs/#{Shakapacker.manifest.lookup!('application.js')}"
274
+ css_path = "/packs/#{Shakapacker.manifest.lookup!('application.css')}"
275
+ links << "<#{js_path}>; rel=preload; as=script"
276
+ links << "<#{css_path}>; rel=preload; as=style"
277
+
278
+ # Critical images (for LCP - Largest Contentful Paint)
279
+ links << "<#{view_context.asset_path('hero.jpg')}>; rel=preload; as=image"
280
+
281
+ # Send ONE HTTP 103 response with all hints
282
+ request.send_early_hints("Link" => links.join(", "))
283
+ end
284
+
285
+ def show
286
+ # Early hints already sent, browser downloading assets in parallel
287
+ @post = Post.find(params[:id])
288
+ end
289
+ end
290
+ ```
291
+
292
+ **When to use:**
293
+
294
+ - Pages with hero images affecting LCP (Largest Contentful Paint)
295
+ - Videos that must load quickly
296
+ - Critical fonts not in pack bundles
297
+
298
+ ### Option 2: HTML Preload Links (Simpler, No Early Hints)
299
+
300
+ Use Rails' `preload_link_tag` to add `<link rel="preload">` in the HTML:
301
+
302
+ ```erb
303
+ <%# app/views/layouts/application.html.erb %>
304
+ <!DOCTYPE html>
305
+ <html>
306
+ <head>
307
+ <%# Shakapacker sends early hints for packs %>
308
+ <%= stylesheet_pack_tag 'application' %>
309
+
310
+ <%# Preload link in HTML (no HTTP 103, but still speeds up loading) %>
311
+ <%= preload_link_tag asset_path('hero.jpg'), as: 'image' %>
312
+ </head>
313
+ <body>
314
+ <%= yield %>
315
+ <%= javascript_pack_tag 'application' %>
316
+ </body>
317
+ </html>
318
+ ```
319
+
320
+ **When to use:**
321
+
322
+ - Images that don't affect LCP
323
+ - Less critical assets
324
+ - Simpler implementation preferred
325
+
326
+ **Note:** `preload_link_tag` only adds HTML `<link>` tags - it does NOT send HTTP 103 Early Hints.
327
+
328
+ ---
329
+
330
+ ## Requirements & Limitations
331
+
332
+ > **📚 Full Requirements:** See the [main documentation](early_hints.md#requirements) for complete browser and server requirements. This section covers limitations specific to the manual API.
333
+
334
+ **IMPORTANT:** Understand these limitations when using the manual API:
335
+
336
+ ### Architecture: Proxy Required for HTTP/2
337
+
338
+ **Standard production architecture for Early Hints:**
339
+
340
+ ```
341
+ Browser (HTTP/2)
342
+
343
+ Proxy (Thruster ✅, nginx ✅, Cloudflare ✅)
344
+ ├─ Receives HTTP/2
345
+ ├─ Translates to HTTP/1.1
346
+
347
+ Puma (HTTP/1.1 with --early-hints flag)
348
+ ├─ Sends HTTP/1.1 103 Early Hints ✅
349
+ ├─ Sends HTTP/1.1 200 OK
350
+
351
+ Proxy
352
+ ├─ Translates to HTTP/2
353
+
354
+ Browser (HTTP/2 103) ✅
355
+ ```
356
+
357
+ **Key insights:**
358
+
359
+ - Puma always runs HTTP/1.1 and requires `--early-hints` flag
360
+ - The proxy handles HTTP/2 for external clients
361
+ - **NOT all proxies support early hints** (Control Plane ❌, AWS ALB ❌)
362
+
363
+ ### Puma Limitation: HTTP/1.1 Only
364
+
365
+ **Puma ONLY supports HTTP/1.1 Early Hints** (not HTTP/2). This is a Rack/Puma limitation, and **there are no plans to add HTTP/2 support to Puma**.
366
+
367
+ - ✅ **Works**: Puma 5+ with HTTP/1.1
368
+ - ❌ **Doesn't work**: Puma with HTTP/2 (h2)
369
+ - ✅ **Solution**: Use a proxy in front of Puma (Thruster, nginx, etc.)
370
+
371
+ **This is the expected architecture** - there's always something in front of Puma to handle HTTP/2 translation in production.
372
+
373
+ ### Browser Behavior
374
+
375
+ **Browsers only process the FIRST `HTTP/1.1 103` response.**
376
+
377
+ - Shakapacker sends ONE 103 response with ALL hints (JS + CSS combined)
378
+ - Subsequent 103 responses are ignored by browsers
379
+ - This is by design per the HTTP 103 spec
380
+
381
+ ### Testing Locally
382
+
383
+ > **📚 Full Testing Guide:** See the [Feature Testing Guide](feature_testing.md#http-103-early-hints) for comprehensive testing instructions with browser DevTools and curl.
384
+
385
+ **Step 1: Enable early hints in your test environment**
386
+
387
+ ```yaml
388
+ # config/shakapacker.yml
389
+ development: # or production
390
+ early_hints:
391
+ enabled: true
392
+ debug: true # Shows hints in HTML comments
393
+ ```
394
+
395
+ **Step 2: Start Rails with Puma's `--early-hints` flag**
396
+
397
+ ```bash
398
+ # Option 1: Test in development (if enabled above)
399
+ bundle exec puma --early-hints
400
+
401
+ # Option 2: Test in production mode locally (more realistic)
402
+ RAILS_ENV=production rails assets:precompile # Compile assets first
403
+ RAILS_ENV=production bundle exec puma --early-hints -e production
404
+ ```
405
+
406
+ **Step 3: Test with curl**
407
+
408
+ ```bash
409
+ # Use HTTP/1.1 (NOT HTTP/2)
410
+ curl -v http://localhost:3000/
411
+
412
+ # Look for this in output:
413
+ < HTTP/1.1 103 Early Hints
414
+ < link: </packs/application-abc123.js>; rel=preload; as=script
415
+ < link: </packs/application-abc123.css>; rel=preload; as=style
416
+ <
417
+ < HTTP/1.1 200 OK
418
+ ```
419
+
420
+ **Important notes:**
421
+
422
+ - Use `http://` (not `https://`) for local testing
423
+ - Puma dev mode uses HTTP/1.1 (not HTTP/2)
424
+ - Test in production mode for realistic asset paths with content hashes
425
+ - Early hints must be `enabled: true` for the environment you're testing
426
+
427
+ ### Production Setup
428
+
429
+ > **📚 Production Setup:** See the [main documentation](early_hints.md#requirements) for complete production setup instructions including Puma configuration, proxy setup (Thruster, nginx, Cloudflare), and troubleshooting proxy issues.
430
+
431
+ **Quick checklist:**
432
+
433
+ - Puma 5+ with `--early-hints` flag (REQUIRED)
434
+ - HTTP/2-capable proxy (Thruster ✅, nginx ✅, Cloudflare ✅, Control Plane ❌, AWS ALB ❌)
435
+ - Rails 5.2+
436
+
437
+ ---
438
+
439
+ ## Troubleshooting
440
+
441
+ > **📚 Complete Troubleshooting:** See the [main documentation](early_hints.md#troubleshooting) for comprehensive troubleshooting including debug mode, proxy configuration, and performance optimization.
442
+
443
+ Quick debugging steps:
444
+
445
+ 1. Enable `debug: true` in shakapacker.yml to see hints in HTML comments
446
+ 2. Verify Puma started with `--early-hints` flag
447
+ 3. Test with `curl -v http://localhost:3000/` to see if Puma sends 103 responses
448
+ 4. Check if your proxy strips 103 responses (Control Plane ❌, AWS ALB ❌)
449
+
450
+ ### Reference
451
+
452
+ - [Main Early Hints Documentation](early_hints.md)
453
+ - [Feature Testing Guide](feature_testing.md#http-103-early-hints)
454
+ - [Rails 103 Early Hints Analysis](https://island94.org/2025/10/rails-103-early-hints-could-be-better-maybe-doesn-t-matter)