Skip to content

Commit fa70b17

Browse files
committed
eager prefetch via config
1 parent 95434d6 commit fa70b17

File tree

5 files changed

+843
-0
lines changed

5 files changed

+843
-0
lines changed

config/inertia.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,12 @@
6464

6565
],
6666

67+
'eager_prefetch' => [
68+
69+
'strategy' => null,
70+
71+
'chunks' => 3,
72+
73+
],
74+
6775
];

src/ServiceProvider.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Inertia;
44

55
use Illuminate\Foundation\Testing\TestResponse as LegacyTestResponse;
6+
use Illuminate\Foundation\Vite;
67
use Illuminate\Http\Request;
78
use Illuminate\Routing\Router;
89
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
@@ -39,6 +40,13 @@ public function register(): void
3940
$app['config']->get('inertia.testing.page_extensions')
4041
);
4142
});
43+
44+
if (config('inertia.eager_prefetch.strategy', false)) {
45+
$this->app->singleton(Vite::class, fn () => (new ViteEagerPrefetch())->usePrefetchStrategy(
46+
config('inertia.eager_prefetch.strategy'),
47+
config('inertia.eager_prefetch.chunks')
48+
));
49+
}
4250
}
4351

4452
public function boot(): void

src/ViteEagerPrefetch.php

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
namespace Inertia;
4+
5+
use Illuminate\Foundation\Vite;
6+
use Illuminate\Support\HtmlString;
7+
use Illuminate\Support\Js;
8+
9+
class ViteEagerPrefetch extends Vite
10+
{
11+
/**
12+
* The prefetching strategy to use.
13+
*
14+
* @var 'waterfall'|'aggressive'
15+
*/
16+
protected $prefetchStrategy = 'waterfall';
17+
18+
/**
19+
* When using the "waterfall" strategy, the count of assets to load at one time.
20+
*
21+
* @param int
22+
*/
23+
protected $prefetchChunks = 3;
24+
25+
/**
26+
* Set the prefetching strategy.
27+
*
28+
* @param 'waterfall'|'aggressive' $strategy
29+
* @param ...mixed $config
30+
*/
31+
public function usePrefetchStrategy(string $strategy, mixed ...$config): static
32+
{
33+
$this->prefetchStrategy = $strategy;
34+
35+
if ($strategy === 'waterfall') {
36+
$this->prefetchChunks = $config[0] ?? 3;
37+
}
38+
39+
return $this;
40+
}
41+
42+
/**
43+
* Generate Vite tags for an entrypoint.
44+
*
45+
* @param string|string[] $entrypoints
46+
* @param string|null $buildDirectory
47+
* @return \Illuminate\Support\HtmlString
48+
*/
49+
public function __invoke($entrypoints, $buildDirectory = null)
50+
{
51+
$manifest = $this->manifest($buildDirectory ??= $this->buildDirectory);
52+
$base = parent::__invoke($entrypoints, $buildDirectory);
53+
54+
if ($this->isRunningHot()) {
55+
return $base;
56+
}
57+
58+
$discoveredImports = [];
59+
60+
return collect($entrypoints)
61+
->flatMap(fn ($entrypoint) => collect($manifest[$entrypoint]['dynamicImports'] ?? [])
62+
->map(fn ($import) => $manifest[$import])
63+
->filter(fn ($chunk) => str_ends_with($chunk['file'], '.js') || str_ends_with($chunk['file'], '.css'))
64+
->flatMap($f = function ($chunk) use (&$f, $manifest, &$discoveredImports) {
65+
return collect([...$chunk['imports'] ?? [], ...$chunk['dynamicImports'] ?? []])
66+
->reject(function ($import) use (&$discoveredImports) {
67+
if (isset($discoveredImports[$import])) {
68+
return true;
69+
}
70+
71+
return ! $discoveredImports[$import] = true;
72+
})
73+
->reduce(
74+
fn ($chunks, $import) => $chunks->merge(
75+
$f($manifest[$import])
76+
),
77+
collect([$chunk])
78+
)
79+
->merge(collect($chunk['css'] ?? [])->map(
80+
fn ($css) => collect($manifest)->first(fn ($chunk) => $chunk['file'] === $css) ?? [
81+
'file' => $css,
82+
],
83+
));
84+
})
85+
->map(function ($chunk) use ($buildDirectory, $manifest) {
86+
return collect([
87+
...$this->resolvePreloadTagAttributes(
88+
$chunk['src'] ?? null,
89+
$url = $this->assetPath("{$buildDirectory}/{$chunk['file']}"),
90+
$chunk,
91+
$manifest,
92+
),
93+
'rel' => 'prefetch',
94+
'href' => $url,
95+
])->reject(
96+
fn ($value) => in_array($value, [null, false], true)
97+
)->mapWithKeys(fn ($value, $key) => [
98+
$key = (is_int($key) ? $value : $key) => $value === true ? $key : $value,
99+
])->all();
100+
})
101+
->reject(fn ($attributes) => isset($this->preloadedAssets[$attributes['href']])))
102+
->unique('href')
103+
->values()
104+
->pipe(fn ($assets) => with(Js::from($assets), fn ($assets) => match ($this->prefetchStrategy) {
105+
'waterfall' => new HtmlString($base.<<<HTML
106+
107+
<script>
108+
window.addEventListener('load', () => window.setTimeout(() => {
109+
const linkTemplate = document.createElement('link')
110+
linkTemplate.rel = 'prefetch'
111+
112+
const makeLink = (asset) => {
113+
const link = linkTemplate.cloneNode()
114+
115+
Object.keys(asset).forEach((attribute) => {
116+
link.setAttribute(attribute, asset[attribute])
117+
})
118+
119+
return link
120+
}
121+
122+
const loadNext = (assets, count) => window.setTimeout(() => {
123+
if (count > assets.length) {
124+
count = assets.length
125+
126+
if (count === 0) {
127+
return
128+
}
129+
}
130+
131+
const fragment = new DocumentFragment
132+
133+
while (count > 0) {
134+
const link = makeLink(assets.shift())
135+
fragment.append(link)
136+
count--
137+
138+
if (assets.length) {
139+
link.onload = () => loadNext(assets, 1)
140+
link.error = () => loadNext(assets, 1)
141+
}
142+
}
143+
144+
document.head.append(fragment)
145+
})
146+
147+
loadNext({$assets}, {$this->prefetchChunks})
148+
}))
149+
</script>
150+
HTML),
151+
'aggressive' => new HtmlString($base.<<<HTML
152+
153+
<script>
154+
window.addEventListener('load', () => window.setTimeout(() => {
155+
const linkTemplate = document.createElement('link')
156+
linkTemplate.rel = 'prefetch'
157+
158+
const makeLink = (asset) => {
159+
const link = linkTemplate.cloneNode()
160+
161+
Object.keys(asset).forEach((attribute) => {
162+
link.setAttribute(attribute, asset[attribute])
163+
})
164+
165+
return link
166+
}
167+
168+
const fragment = new DocumentFragment
169+
{$assets}.forEach((asset) => fragment.append(makeLink(asset)))
170+
document.head.append(fragment)
171+
}))
172+
</script>
173+
HTML),
174+
}));
175+
}
176+
}

0 commit comments

Comments
 (0)