shakapacker 9.3.0.beta.4 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +19 -0
- data/docs/configuration.md +35 -0
- data/docs/early_hints.md +440 -0
- data/docs/early_hints_new_api.md +700 -0
- data/docs/feature_testing.md +492 -0
- data/lib/install/config/shakapacker.yml +27 -0
- data/lib/shakapacker/configuration.rb +4 -0
- data/lib/shakapacker/helper.rb +409 -14
- data/lib/shakapacker/railtie.rb +4 -0
- data/lib/shakapacker/version.rb +1 -1
- data/package/configExporter/buildValidator.ts +1 -2
- data/package/configExporter/cli.ts +2 -1
- data/package-lock.json +2 -2
- data/package.json +1 -1
- data/scripts/remove-use-strict.js +0 -1
- data/test/package/rules/babel.test.js +1 -0
- data/test/package/rules/swc.test.js +1 -0
- metadata +5 -2
@@ -0,0 +1,700 @@
|
|
1
|
+
# HTTP 103 Early Hints - New API
|
2
|
+
|
3
|
+
## ⚠️ IMPORTANT: Performance Testing Required
|
4
|
+
|
5
|
+
**Early hints can improve OR hurt performance** depending on your application:
|
6
|
+
|
7
|
+
- ✅ **May help**: Large JS bundles (>500KB), slow controllers (>300ms), fast CDN
|
8
|
+
- ❌ **May hurt**: Large images as LCP (Largest Contentful Paint), small JS bundles
|
9
|
+
- ⚠️ **Test before deploying**: Measure LCP and Time to Interactive before/after enabling
|
10
|
+
|
11
|
+
**How to test:**
|
12
|
+
|
13
|
+
1. Enable early hints in production for 10% of traffic
|
14
|
+
2. Measure Core Web Vitals (LCP, FCP, TTI) for both groups
|
15
|
+
3. Only keep enabled if metrics improve
|
16
|
+
|
17
|
+
See [Troubleshooting](#performance-got-worse) if early hints decrease performance.
|
18
|
+
|
19
|
+
---
|
20
|
+
|
21
|
+
## Prerequisites
|
22
|
+
|
23
|
+
Before implementing early hints, verify you have:
|
24
|
+
|
25
|
+
1. **Puma 5+** with `--early-hints` flag (REQUIRED)
|
26
|
+
2. **HTTP/2-capable proxy** in front of Puma:
|
27
|
+
- ✅ Thruster (Rails 8 default)
|
28
|
+
- ✅ nginx 1.13+
|
29
|
+
- ✅ Cloudflare (paid plans)
|
30
|
+
- ❌ Control Plane (strips 103 responses)
|
31
|
+
- ❌ AWS ALB/ELB (strips 103 responses)
|
32
|
+
3. **Rails 5.2+** (for `request.send_early_hints` API)
|
33
|
+
|
34
|
+
**Critical**: Puma requires the `--early-hints` flag to send HTTP 103:
|
35
|
+
|
36
|
+
```bash
|
37
|
+
# Procfile / Dockerfile
|
38
|
+
web: bundle exec puma --early-hints -C config/puma.rb
|
39
|
+
```
|
40
|
+
|
41
|
+
Without this flag, early hints will NOT work. See [Setup](#production-setup) for details.
|
42
|
+
|
43
|
+
---
|
44
|
+
|
45
|
+
## Quick Start
|
46
|
+
|
47
|
+
### Pattern 1: Automatic (Default)
|
48
|
+
|
49
|
+
By default, `javascript_pack_tag` and `stylesheet_pack_tag` automatically send early hints when early hints are enabled in config:
|
50
|
+
|
51
|
+
```yaml
|
52
|
+
# config/shakapacker.yml
|
53
|
+
production:
|
54
|
+
early_hints:
|
55
|
+
enabled: true
|
56
|
+
```
|
57
|
+
|
58
|
+
```erb
|
59
|
+
<%# app/views/layouts/application.html.erb %>
|
60
|
+
<!DOCTYPE html>
|
61
|
+
<html>
|
62
|
+
<head>
|
63
|
+
<%# Automatically sends early hints for application pack CSS %>
|
64
|
+
<%= stylesheet_pack_tag 'application' %>
|
65
|
+
</head>
|
66
|
+
<body>
|
67
|
+
<%= yield %>
|
68
|
+
<%# Automatically sends early hints for application pack JS %>
|
69
|
+
<%= javascript_pack_tag 'application' %>
|
70
|
+
</body>
|
71
|
+
</html>
|
72
|
+
```
|
73
|
+
|
74
|
+
**How it works:**
|
75
|
+
|
76
|
+
- When `stylesheet_pack_tag` is called, it automatically sends CSS early hints
|
77
|
+
- When `javascript_pack_tag` is called, it automatically sends JS early hints
|
78
|
+
- Combines queue (from `append_*_pack_tag`) + direct args
|
79
|
+
- Default: `rel=preload` for all packs
|
80
|
+
|
81
|
+
---
|
82
|
+
|
83
|
+
## Pattern 2: Per-Pack Customization in Layout
|
84
|
+
|
85
|
+
Customize hint handling per pack using a hash:
|
86
|
+
|
87
|
+
```erb
|
88
|
+
<%# app/views/layouts/application.html.erb %>
|
89
|
+
<!DOCTYPE html>
|
90
|
+
<html>
|
91
|
+
<head>
|
92
|
+
<%# Mixed handling: preload application, prefetch vendor %>
|
93
|
+
<%= stylesheet_pack_tag 'application', 'vendor',
|
94
|
+
early_hints: { 'application' => 'preload', 'vendor' => 'prefetch' } %>
|
95
|
+
</head>
|
96
|
+
<body>
|
97
|
+
<%= yield %>
|
98
|
+
<%# Disable early hints for this tag %>
|
99
|
+
<%= javascript_pack_tag 'application', early_hints: false %>
|
100
|
+
</body>
|
101
|
+
</html>
|
102
|
+
```
|
103
|
+
|
104
|
+
**Options:**
|
105
|
+
|
106
|
+
- `"preload"` - High priority (default)
|
107
|
+
- `"prefetch"` - Low priority
|
108
|
+
- `false` or `"none"` - Disabled
|
109
|
+
|
110
|
+
---
|
111
|
+
|
112
|
+
## Pattern 3: Controller Override (Before Expensive Work)
|
113
|
+
|
114
|
+
Send hints manually in controller BEFORE expensive work to maximize parallelism:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class PostsController < ApplicationController
|
118
|
+
def show
|
119
|
+
# Send hints BEFORE expensive work
|
120
|
+
send_pack_early_hints({
|
121
|
+
"application" => { js: "preload", css: "preload" },
|
122
|
+
"admin" => { js: "prefetch", css: "none" }
|
123
|
+
})
|
124
|
+
|
125
|
+
# Browser now downloading assets while we do expensive work
|
126
|
+
@post = Post.includes(:comments, :author, :tags).find(params[:id])
|
127
|
+
@related = @post.find_related_posts(limit: 10) # Expensive query
|
128
|
+
# ... more work ...
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
**Timeline:**
|
134
|
+
|
135
|
+
1. Request arrives
|
136
|
+
2. `send_pack_early_hints` called → HTTP 103 sent immediately
|
137
|
+
3. Browser starts downloading assets
|
138
|
+
4. Rails continues with expensive queries (IN PARALLEL with browser downloads)
|
139
|
+
5. View renders
|
140
|
+
6. HTTP 200 sent with full HTML
|
141
|
+
7. Assets already downloaded = faster page load
|
142
|
+
|
143
|
+
**Benefits:**
|
144
|
+
|
145
|
+
- ✅ Parallelizes browser downloads with server processing
|
146
|
+
- ✅ Can save 200-500ms on pages with slow controllers
|
147
|
+
- ✅ Most valuable for pages with expensive queries/API calls
|
148
|
+
|
149
|
+
---
|
150
|
+
|
151
|
+
## Pattern 4: View Override
|
152
|
+
|
153
|
+
Views can use `append_*_pack_tag` to add packs dynamically:
|
154
|
+
|
155
|
+
```erb
|
156
|
+
<%# app/views/posts/edit.html.erb %>
|
157
|
+
<% append_javascript_pack_tag 'admin_tools' %>
|
158
|
+
|
159
|
+
<div class="post-editor">
|
160
|
+
<%# ... editor UI ... %>
|
161
|
+
</div>
|
162
|
+
```
|
163
|
+
|
164
|
+
```erb
|
165
|
+
<%# app/views/layouts/application.html.erb %>
|
166
|
+
<!DOCTYPE html>
|
167
|
+
<html>
|
168
|
+
<head>
|
169
|
+
<%= stylesheet_pack_tag 'application' %>
|
170
|
+
</head>
|
171
|
+
<body>
|
172
|
+
<%= yield %> <%# View has run, admin_tools added to queue %>
|
173
|
+
|
174
|
+
<%# Sends hints for BOTH application + admin_tools %>
|
175
|
+
<%= javascript_pack_tag 'application' %>
|
176
|
+
</body>
|
177
|
+
</html>
|
178
|
+
```
|
179
|
+
|
180
|
+
**How it works:**
|
181
|
+
|
182
|
+
- Views call `append_javascript_pack_tag('admin_tools')`
|
183
|
+
- Layout calls `javascript_pack_tag('application')`
|
184
|
+
- Helper combines: `['application', 'admin_tools']`
|
185
|
+
- Sends hints for ALL packs automatically
|
186
|
+
|
187
|
+
---
|
188
|
+
|
189
|
+
## Configuration
|
190
|
+
|
191
|
+
```yaml
|
192
|
+
# config/shakapacker.yml
|
193
|
+
production:
|
194
|
+
early_hints:
|
195
|
+
enabled: true # Master switch (default: false)
|
196
|
+
debug: true # Show HTML comments with debug info (default: false)
|
197
|
+
```
|
198
|
+
|
199
|
+
---
|
200
|
+
|
201
|
+
## Duplicate Prevention
|
202
|
+
|
203
|
+
Hints are automatically prevented from being sent twice:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
# Controller
|
207
|
+
def show
|
208
|
+
send_pack_early_hints({ "application" => { js: "preload", css: "preload" } })
|
209
|
+
# ... expensive work ...
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
```erb
|
214
|
+
<%# Layout %>
|
215
|
+
<%= javascript_pack_tag 'application' %>
|
216
|
+
<%# Won't send duplicate hint - already sent in controller %>
|
217
|
+
```
|
218
|
+
|
219
|
+
**How it works:**
|
220
|
+
|
221
|
+
- Tracks which packs have sent JS hints: `@early_hints_javascript = {}`
|
222
|
+
- Tracks which packs have sent CSS hints: `@early_hints_stylesheets = {}`
|
223
|
+
- Skips sending hints for packs already sent
|
224
|
+
|
225
|
+
---
|
226
|
+
|
227
|
+
## When to Use Each Pattern
|
228
|
+
|
229
|
+
### Pattern 1 (Automatic) - Best for:
|
230
|
+
|
231
|
+
- Simple apps with consistent performance
|
232
|
+
- Small/medium JS bundles (<500KB)
|
233
|
+
- Fast controllers (<100ms)
|
234
|
+
|
235
|
+
### Pattern 2 (Per-Pack) - Best for:
|
236
|
+
|
237
|
+
- Mixed vendor bundles (preload critical, prefetch non-critical)
|
238
|
+
- Different handling for different packs
|
239
|
+
- Layout-specific optimizations
|
240
|
+
|
241
|
+
### Pattern 3 (Controller) - Best for:
|
242
|
+
|
243
|
+
- Slow controllers with expensive queries (>300ms)
|
244
|
+
- Large JS bundles (>500KB)
|
245
|
+
- APIs calls in controller
|
246
|
+
- Maximum parallelism needed
|
247
|
+
|
248
|
+
### Pattern 4 (View Override) - Best for:
|
249
|
+
|
250
|
+
- Admin sections with extra packs
|
251
|
+
- Feature flags determining packs
|
252
|
+
- Page-specific bundles
|
253
|
+
|
254
|
+
---
|
255
|
+
|
256
|
+
## Full Example: Mixed Patterns
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
# app/controllers/posts_controller.rb
|
260
|
+
class PostsController < ApplicationController
|
261
|
+
def index
|
262
|
+
# Fast controller, use automatic hints
|
263
|
+
end
|
264
|
+
|
265
|
+
def show
|
266
|
+
# Slow controller, send hints early
|
267
|
+
send_pack_early_hints({
|
268
|
+
"application" => { js: "preload", css: "preload" }
|
269
|
+
})
|
270
|
+
|
271
|
+
# Expensive work happens in parallel with browser downloads
|
272
|
+
@post = Post.includes(:comments, :author).find(params[:id])
|
273
|
+
end
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
```erb
|
278
|
+
<%# app/views/posts/show.html.erb %>
|
279
|
+
<% if current_user&.admin? %>
|
280
|
+
<% append_javascript_pack_tag 'admin_tools' %>
|
281
|
+
<% end %>
|
282
|
+
```
|
283
|
+
|
284
|
+
```erb
|
285
|
+
<%# app/views/layouts/application.html.erb %>
|
286
|
+
<!DOCTYPE html>
|
287
|
+
<html>
|
288
|
+
<head>
|
289
|
+
<%# Automatic CSS hints for application %>
|
290
|
+
<%= stylesheet_pack_tag 'application' %>
|
291
|
+
</head>
|
292
|
+
<body>
|
293
|
+
<%= yield %>
|
294
|
+
|
295
|
+
<%# Automatic JS hints for application + admin_tools (if appended) %>
|
296
|
+
<%# Won't duplicate hints already sent in controller %>
|
297
|
+
<%= javascript_pack_tag 'application' %>
|
298
|
+
</body>
|
299
|
+
</html>
|
300
|
+
```
|
301
|
+
|
302
|
+
---
|
303
|
+
|
304
|
+
## Preloading Non-Pack Assets (Images, Videos, Fonts)
|
305
|
+
|
306
|
+
**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:
|
307
|
+
|
308
|
+
### Option 1: Manual Early Hints (For LCP/Critical Assets)
|
309
|
+
|
310
|
+
**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.
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
class PostsController < ApplicationController
|
314
|
+
before_action :send_critical_early_hints, only: [:show]
|
315
|
+
|
316
|
+
private
|
317
|
+
|
318
|
+
def send_critical_early_hints
|
319
|
+
# Build all early hints in ONE call (packs + images)
|
320
|
+
links = []
|
321
|
+
|
322
|
+
# Pack assets (using Shakapacker manifest)
|
323
|
+
js_path = "/packs/#{Shakapacker.manifest.lookup!('application.js')}"
|
324
|
+
css_path = "/packs/#{Shakapacker.manifest.lookup!('application.css')}"
|
325
|
+
links << "<#{js_path}>; rel=preload; as=script"
|
326
|
+
links << "<#{css_path}>; rel=preload; as=style"
|
327
|
+
|
328
|
+
# Critical images (for LCP - Largest Contentful Paint)
|
329
|
+
links << "<#{view_context.asset_path('hero.jpg')}>; rel=preload; as=image"
|
330
|
+
|
331
|
+
# Send ONE HTTP 103 response with all hints
|
332
|
+
request.send_early_hints("Link" => links.join(", "))
|
333
|
+
end
|
334
|
+
|
335
|
+
def show
|
336
|
+
# Early hints already sent, browser downloading assets in parallel
|
337
|
+
@post = Post.find(params[:id])
|
338
|
+
end
|
339
|
+
end
|
340
|
+
```
|
341
|
+
|
342
|
+
**When to use:**
|
343
|
+
|
344
|
+
- Pages with hero images affecting LCP (Largest Contentful Paint)
|
345
|
+
- Videos that must load quickly
|
346
|
+
- Critical fonts not in pack bundles
|
347
|
+
|
348
|
+
### Option 2: HTML Preload Links (Simpler, No Early Hints)
|
349
|
+
|
350
|
+
Use Rails' `preload_link_tag` to add `<link rel="preload">` in the HTML:
|
351
|
+
|
352
|
+
```erb
|
353
|
+
<%# app/views/layouts/application.html.erb %>
|
354
|
+
<!DOCTYPE html>
|
355
|
+
<html>
|
356
|
+
<head>
|
357
|
+
<%# Shakapacker sends early hints for packs %>
|
358
|
+
<%= stylesheet_pack_tag 'application' %>
|
359
|
+
|
360
|
+
<%# Preload link in HTML (no HTTP 103, but still speeds up loading) %>
|
361
|
+
<%= preload_link_tag asset_path('hero.jpg'), as: 'image' %>
|
362
|
+
</head>
|
363
|
+
<body>
|
364
|
+
<%= yield %>
|
365
|
+
<%= javascript_pack_tag 'application' %>
|
366
|
+
</body>
|
367
|
+
</html>
|
368
|
+
```
|
369
|
+
|
370
|
+
**When to use:**
|
371
|
+
|
372
|
+
- Images that don't affect LCP
|
373
|
+
- Less critical assets
|
374
|
+
- Simpler implementation preferred
|
375
|
+
|
376
|
+
**Note:** `preload_link_tag` only adds HTML `<link>` tags - it does NOT send HTTP 103 Early Hints.
|
377
|
+
|
378
|
+
---
|
379
|
+
|
380
|
+
## Requirements & Limitations
|
381
|
+
|
382
|
+
**IMPORTANT:** Before implementing Early Hints, understand these limitations:
|
383
|
+
|
384
|
+
### Architecture: Proxy Required for HTTP/2
|
385
|
+
|
386
|
+
**Standard production architecture for Early Hints:**
|
387
|
+
|
388
|
+
```
|
389
|
+
Browser (HTTP/2)
|
390
|
+
↓
|
391
|
+
Proxy (Thruster ✅, nginx ✅, Cloudflare ✅)
|
392
|
+
├─ Receives HTTP/2
|
393
|
+
├─ Translates to HTTP/1.1
|
394
|
+
↓
|
395
|
+
Puma (HTTP/1.1 with --early-hints flag)
|
396
|
+
├─ Sends HTTP/1.1 103 Early Hints ✅
|
397
|
+
├─ Sends HTTP/1.1 200 OK
|
398
|
+
↓
|
399
|
+
Proxy
|
400
|
+
├─ Translates to HTTP/2
|
401
|
+
↓
|
402
|
+
Browser (HTTP/2 103) ✅
|
403
|
+
```
|
404
|
+
|
405
|
+
**Key insights:**
|
406
|
+
|
407
|
+
- Puma always runs HTTP/1.1 and requires `--early-hints` flag
|
408
|
+
- The proxy handles HTTP/2 for external clients
|
409
|
+
- **NOT all proxies support early hints** (Control Plane ❌, AWS ALB ❌)
|
410
|
+
|
411
|
+
### Puma Limitation: HTTP/1.1 Only
|
412
|
+
|
413
|
+
**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**.
|
414
|
+
|
415
|
+
- ✅ **Works**: Puma 5+ with HTTP/1.1
|
416
|
+
- ❌ **Doesn't work**: Puma with HTTP/2 (h2)
|
417
|
+
- ✅ **Solution**: Use a proxy in front of Puma (Thruster, nginx, etc.)
|
418
|
+
|
419
|
+
**This is the expected architecture** - there's always something in front of Puma to handle HTTP/2 translation in production.
|
420
|
+
|
421
|
+
### Browser Behavior
|
422
|
+
|
423
|
+
**Browsers only process the FIRST `HTTP/1.1 103` response.**
|
424
|
+
|
425
|
+
- Shakapacker sends ONE 103 response with ALL hints (JS + CSS combined)
|
426
|
+
- Subsequent 103 responses are ignored by browsers
|
427
|
+
- This is by design per the HTTP 103 spec
|
428
|
+
|
429
|
+
### Browser Support
|
430
|
+
|
431
|
+
- Chrome/Firefox 103+
|
432
|
+
- Safari 16.4+
|
433
|
+
- Gracefully degrades if not supported
|
434
|
+
|
435
|
+
### Testing Locally
|
436
|
+
|
437
|
+
**Step 1: Enable early hints in your test environment**
|
438
|
+
|
439
|
+
```yaml
|
440
|
+
# config/shakapacker.yml
|
441
|
+
development: # or production
|
442
|
+
early_hints:
|
443
|
+
enabled: true
|
444
|
+
debug: true # Shows hints in HTML comments
|
445
|
+
```
|
446
|
+
|
447
|
+
**Step 2: Start Rails with Puma's `--early-hints` flag**
|
448
|
+
|
449
|
+
```bash
|
450
|
+
# Option 1: Test in development (if enabled above)
|
451
|
+
bundle exec puma --early-hints
|
452
|
+
|
453
|
+
# Option 2: Test in production mode locally (more realistic)
|
454
|
+
RAILS_ENV=production rails assets:precompile # Compile assets first
|
455
|
+
RAILS_ENV=production bundle exec puma --early-hints -e production
|
456
|
+
```
|
457
|
+
|
458
|
+
**Step 3: Test with curl**
|
459
|
+
|
460
|
+
```bash
|
461
|
+
# Use HTTP/1.1 (NOT HTTP/2)
|
462
|
+
curl -v http://localhost:3000/
|
463
|
+
|
464
|
+
# Look for this in output:
|
465
|
+
< HTTP/1.1 103 Early Hints
|
466
|
+
< link: </packs/application-abc123.js>; rel=preload; as=script
|
467
|
+
< link: </packs/application-abc123.css>; rel=preload; as=style
|
468
|
+
<
|
469
|
+
< HTTP/1.1 200 OK
|
470
|
+
```
|
471
|
+
|
472
|
+
**Important notes:**
|
473
|
+
|
474
|
+
- Use `http://` (not `https://`) for local testing
|
475
|
+
- Puma dev mode uses HTTP/1.1 (not HTTP/2)
|
476
|
+
- Test in production mode for realistic asset paths with content hashes
|
477
|
+
- Early hints must be `enabled: true` for the environment you're testing
|
478
|
+
|
479
|
+
### Production Setup
|
480
|
+
|
481
|
+
#### Thruster (Rails 8+ Default)
|
482
|
+
|
483
|
+
**Recommended**: Use [Thruster](https://github.com/basecamp/thruster) in front of Puma (Rails 8 default).
|
484
|
+
|
485
|
+
Thruster handles HTTP/2 → HTTP/1.1 translation automatically. No configuration needed - Early Hints just work.
|
486
|
+
|
487
|
+
```dockerfile
|
488
|
+
# Dockerfile (Rails 8 default)
|
489
|
+
CMD ["bundle", "exec", "thrust", "./bin/rails", "server"]
|
490
|
+
```
|
491
|
+
|
492
|
+
Thruster will:
|
493
|
+
|
494
|
+
1. Receive HTTP/2 requests from browsers
|
495
|
+
2. Translate to HTTP/1.1 for Puma
|
496
|
+
3. Pass through HTTP/1.1 103 Early Hints from Puma
|
497
|
+
4. Translate to HTTP/2 103 for browsers
|
498
|
+
|
499
|
+
#### Control Plane
|
500
|
+
|
501
|
+
**Status: Early Hints NOT supported** ❌
|
502
|
+
|
503
|
+
Control Plane's load balancer appears to strip HTTP 103 responses, even with correct configuration:
|
504
|
+
|
505
|
+
- Puma sends HTTP/1.1 103 ✅ (verified locally with curl)
|
506
|
+
- Control Plane LB strips 103 before reaching browser ❌
|
507
|
+
- Workload protocol set to `HTTP` (not `HTTP2`) ✅
|
508
|
+
- Puma started with `--early-hints` flag ✅
|
509
|
+
|
510
|
+
**Recommendation**: If you need early hints, consider:
|
511
|
+
|
512
|
+
- **Thruster** (Rails 8 default, supports early hints)
|
513
|
+
- **Self-hosted nginx** (supports early hints)
|
514
|
+
- **Cloudflare** (paid plans only)
|
515
|
+
- Contact Control Plane support to request early hints support
|
516
|
+
|
517
|
+
#### nginx (Self-Hosted)
|
518
|
+
|
519
|
+
If you want HTTP/2 in production with self-hosted nginx:
|
520
|
+
|
521
|
+
```nginx
|
522
|
+
# /etc/nginx/sites-available/myapp
|
523
|
+
upstream puma {
|
524
|
+
server unix:///var/www/myapp/tmp/sockets/puma.sock;
|
525
|
+
}
|
526
|
+
|
527
|
+
server {
|
528
|
+
listen 443 ssl http2;
|
529
|
+
server_name example.com;
|
530
|
+
|
531
|
+
# SSL certificates
|
532
|
+
ssl_certificate /path/to/cert.pem;
|
533
|
+
ssl_certificate_key /path/to/key.pem;
|
534
|
+
|
535
|
+
location / {
|
536
|
+
proxy_pass http://puma; # Puma uses HTTP/1.1
|
537
|
+
proxy_set_header Host $host;
|
538
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
539
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
540
|
+
|
541
|
+
# CRITICAL: Pass through Early Hints from Puma
|
542
|
+
proxy_pass_header Link;
|
543
|
+
}
|
544
|
+
}
|
545
|
+
```
|
546
|
+
|
547
|
+
nginx will:
|
548
|
+
|
549
|
+
1. Receive HTTP/2 request from browser
|
550
|
+
2. Forward as HTTP/1.1 to Puma
|
551
|
+
3. Receive HTTP/1.1 103 from Puma
|
552
|
+
4. Translate to HTTP/2 103 for browser
|
553
|
+
|
554
|
+
---
|
555
|
+
|
556
|
+
## Troubleshooting
|
557
|
+
|
558
|
+
### Early Hints Not Appearing
|
559
|
+
|
560
|
+
**Step 1: Enable debug mode to see what Puma is sending**
|
561
|
+
|
562
|
+
```yaml
|
563
|
+
# config/shakapacker.yml
|
564
|
+
development:
|
565
|
+
early_hints:
|
566
|
+
enabled: true
|
567
|
+
debug: true # Shows hints in HTML comments
|
568
|
+
```
|
569
|
+
|
570
|
+
Reload your page and check the HTML source for comments like:
|
571
|
+
|
572
|
+
```html
|
573
|
+
<!-- Early hints sent (JS): application=preload -->
|
574
|
+
<!-- Early hints sent (CSS): application=preload -->
|
575
|
+
```
|
576
|
+
|
577
|
+
**If debug shows hints are sent:**
|
578
|
+
|
579
|
+
The issue is with your **proxy/infrastructure**, not Shakapacker. Proceed to Step 2.
|
580
|
+
|
581
|
+
**If debug shows NO hints sent:**
|
582
|
+
|
583
|
+
Check your config:
|
584
|
+
|
585
|
+
- `early_hints.enabled: true` in `config/shakapacker.yml`
|
586
|
+
- Rails 5.2+
|
587
|
+
- Puma 5+
|
588
|
+
- **Puma started with `--early-hints` flag** (REQUIRED!)
|
589
|
+
|
590
|
+
---
|
591
|
+
|
592
|
+
**Step 2: Check if your proxy is stripping 103 responses**
|
593
|
+
|
594
|
+
This is the **most common cause** of missing early hints.
|
595
|
+
|
596
|
+
Test with curl against your local Puma (HTTP/1.1):
|
597
|
+
|
598
|
+
```bash
|
599
|
+
# Direct to Puma (should work)
|
600
|
+
curl -v http://localhost:3000/
|
601
|
+
|
602
|
+
# Look for:
|
603
|
+
< HTTP/1.1 103 Early Hints
|
604
|
+
< link: </packs/application.js>; rel=preload; as=script
|
605
|
+
<
|
606
|
+
< HTTP/1.1 200 OK
|
607
|
+
```
|
608
|
+
|
609
|
+
If you see the 103 response, Puma is working correctly.
|
610
|
+
|
611
|
+
---
|
612
|
+
|
613
|
+
**Step 3: Common proxy issues**
|
614
|
+
|
615
|
+
#### Control Plane
|
616
|
+
|
617
|
+
**Status: NOT supported** ❌
|
618
|
+
|
619
|
+
Control Plane strips HTTP 103 responses. No known workaround. Consider switching to Thruster, nginx, or Cloudflare if you need early hints.
|
620
|
+
|
621
|
+
#### AWS ALB/ELB
|
622
|
+
|
623
|
+
**Not supported** - ALBs strip 103 responses entirely. No workaround except:
|
624
|
+
|
625
|
+
- Skip ALB (not recommended)
|
626
|
+
- Use CloudFront in front (CloudFront supports early hints)
|
627
|
+
|
628
|
+
#### Cloudflare
|
629
|
+
|
630
|
+
Enable "Early Hints" in dashboard:
|
631
|
+
|
632
|
+
```
|
633
|
+
Speed > Optimization > Early Hints: ON
|
634
|
+
```
|
635
|
+
|
636
|
+
**Note:** Paid plans only (Pro/Business/Enterprise).
|
637
|
+
|
638
|
+
#### nginx
|
639
|
+
|
640
|
+
nginx 1.13+ passes 103 responses automatically. Ensure you're using HTTP/2:
|
641
|
+
|
642
|
+
```nginx
|
643
|
+
server {
|
644
|
+
listen 443 ssl http2; # Enable HTTP/2
|
645
|
+
|
646
|
+
location / {
|
647
|
+
proxy_pass http://puma; # Puma uses HTTP/1.1
|
648
|
+
proxy_http_version 1.1; # Required for Puma
|
649
|
+
}
|
650
|
+
}
|
651
|
+
```
|
652
|
+
|
653
|
+
No special configuration needed - nginx automatically translates HTTP/1.1 103 to HTTP/2 103.
|
654
|
+
|
655
|
+
#### Thruster (Rails 8+)
|
656
|
+
|
657
|
+
Thruster handles HTTP/2 → HTTP/1.1 translation automatically. Early hints just work. No configuration needed.
|
658
|
+
|
659
|
+
---
|
660
|
+
|
661
|
+
### Debugging Checklist
|
662
|
+
|
663
|
+
1. ✅ **Config enabled:** `early_hints.enabled: true` in `shakapacker.yml`
|
664
|
+
2. ✅ **Puma `--early-hints` flag:** Puma started with this flag (REQUIRED!)
|
665
|
+
3. ✅ **Debug mode on:** See HTML comments confirming hints sent
|
666
|
+
4. ✅ **Puma 5+:** Early hints require Puma 5+
|
667
|
+
5. ✅ **Rails 5.2+:** `request.send_early_hints` API available
|
668
|
+
6. ✅ **Architecture:** Proxy in front of Puma (Thruster, nginx - NOT Control Plane or AWS ALB)
|
669
|
+
7. ✅ **Puma protocol:** Always HTTP/1.1 (never HTTP/2)
|
670
|
+
8. ✅ **Proxy protocol:** HTTP/2 to browser, HTTP/1.1 to Puma
|
671
|
+
9. ✅ **Browser support:** Chrome 103+, Firefox 103+, Safari 16.4+
|
672
|
+
|
673
|
+
---
|
674
|
+
|
675
|
+
### Performance Got Worse?
|
676
|
+
|
677
|
+
If enabling early hints **decreased** performance:
|
678
|
+
|
679
|
+
**Likely cause:** Page has large images/videos as LCP (Largest Contentful Paint).
|
680
|
+
|
681
|
+
Preloading large JS bundles can delay image downloads, hurting LCP.
|
682
|
+
|
683
|
+
**Fix:**
|
684
|
+
|
685
|
+
```yaml
|
686
|
+
# config/shakapacker.yml
|
687
|
+
production:
|
688
|
+
early_hints:
|
689
|
+
enabled: true
|
690
|
+
css: "prefetch" # Lower priority
|
691
|
+
js: "prefetch" # Lower priority
|
692
|
+
```
|
693
|
+
|
694
|
+
Or disable entirely and use HTML `preload_link_tag` for images instead.
|
695
|
+
|
696
|
+
---
|
697
|
+
|
698
|
+
### Reference
|
699
|
+
|
700
|
+
- [Rails 103 Early Hints Analysis](https://island94.org/2025/10/rails-103-early-hints-could-be-better-maybe-doesn-t-matter)
|