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.
@@ -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)