Skip to content

Commit af10763

Browse files
committed
Use <LinkPreload /> component to preload assets
1 parent 8b051ca commit af10763

File tree

7 files changed

+161
-63
lines changed

7 files changed

+161
-63
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
5+
6+
// Licensed to the .NET Foundation under one or more agreements.
7+
// The .NET Foundation licenses this file to you under the MIT license.
8+
9+
10+
using Microsoft.AspNetCore.Components.Endpoints;
11+
using Microsoft.AspNetCore.Components.Rendering;
12+
13+
namespace Microsoft.AspNetCore.Components;
14+
15+
public class LinkPreload : IComponent
16+
{
17+
private RenderHandle renderHandle;
18+
private List<PreloadAsset>? assets;
19+
20+
[Inject]
21+
internal ResourcePreloadService Service { get; set; }
22+
23+
public void Attach(RenderHandle renderHandle)
24+
{
25+
this.renderHandle = renderHandle;
26+
}
27+
28+
public Task SetParametersAsync(ParameterView parameters)
29+
{
30+
Service.SetPreloadHook(PreloadGroup);
31+
renderHandle.Render(RenderPreloadAssets);
32+
return Task.CompletedTask;
33+
}
34+
35+
private void PreloadGroup(List<PreloadAsset> assets)
36+
{
37+
if (this.assets != null)
38+
{
39+
return;
40+
}
41+
42+
this.assets = assets;
43+
renderHandle.Render(RenderPreloadAssets);
44+
}
45+
46+
private void RenderPreloadAssets(RenderTreeBuilder builder)
47+
{
48+
if (assets == null)
49+
{
50+
return;
51+
}
52+
53+
for (var i = 0; i < assets.Count; i ++)
54+
{
55+
var asset = assets[i];
56+
builder.OpenElement(0, "link");
57+
builder.SetKey(assets[i]);
58+
builder.AddAttribute(1, "href", asset.Url);
59+
builder.AddAttribute(2, "rel", asset.PreloadRel);
60+
if (!string.IsNullOrEmpty(asset.PreloadAs))
61+
{
62+
builder.AddAttribute(3, "as", asset.PreloadAs);
63+
}
64+
if (!string.IsNullOrEmpty(asset.PreloadPriority))
65+
{
66+
builder.AddAttribute(4, "fetchpriority", asset.PreloadPriority);
67+
}
68+
if (!string.IsNullOrEmpty(asset.PreloadCrossorigin))
69+
{
70+
builder.AddAttribute(5, "crossorigin", asset.PreloadCrossorigin);
71+
}
72+
if (!string.IsNullOrEmpty(asset.Integrity))
73+
{
74+
builder.AddAttribute(6, "integrity", asset.Integrity);
75+
}
76+
builder.CloseElement();
77+
}
78+
}
79+
}

src/Components/Endpoints/src/Builder/ResourceCollectionConvention.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,8 @@ public void ApplyConvention(EndpointBuilder eb)
4949
// The user called MapStaticAssets
5050
if (_collection != null && _collectionImportMap != null)
5151
{
52-
int relativeRootDistance = 0;
53-
if (eb is RouteEndpointBuilder reb)
54-
{
55-
// For routes like '/path/to/page' we need to make preload headers as '../../_framework/dotnet.js'
56-
relativeRootDistance = reb.RoutePattern.PathSegments.Count - 1;
57-
}
58-
5952
eb.Metadata.Add(_collection);
60-
eb.Metadata.Add(new ResourcePreloadCollection(_collection, relativeRootDistance));
53+
eb.Metadata.Add(new ResourcePreloadCollection(_collection));
6154

6255
if (_collectionUrl != null)
6356
{
Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Linq;
5-
using System.Text;
6-
using Microsoft.Extensions.Primitives;
7-
84
namespace Microsoft.AspNetCore.Components.Endpoints;
95

106
internal class ResourcePreloadCollection
117
{
12-
private readonly Dictionary<string, StringValues> _storage = new();
8+
private readonly Dictionary<string, List<PreloadAsset>> _storage = new();
139

14-
public ResourcePreloadCollection(ResourceAssetCollection assets, int relativeRootDistance)
10+
public ResourcePreloadCollection(ResourceAssetCollection assets)
1511
{
16-
var headerBuilder = new StringBuilder();
17-
var headers = new Dictionary<string, List<(int Order, string Value)>>();
1812
foreach (var asset in assets)
1913
{
2014
if (asset.Properties == null)
@@ -38,69 +32,81 @@ public ResourcePreloadCollection(ResourceAssetCollection assets, int relativeRoo
3832
continue;
3933
}
4034

41-
var header = CreateHeader(headerBuilder, relativeRootDistance, asset.Url, asset.Properties);
42-
if (!headers.TryGetValue(group, out var groupHeaders))
35+
var preloadAsset = CreateAsset(asset.Url, asset.Properties);
36+
if (!_storage.TryGetValue(group, out var groupHeaders))
4337
{
44-
groupHeaders = headers[group] = new List<(int Order, string Value)>();
38+
groupHeaders = _storage[group] = new List<PreloadAsset>();
4539
}
4640

47-
groupHeaders.Add(header);
41+
groupHeaders.Add(preloadAsset);
4842
}
4943

50-
foreach (var group in headers)
44+
foreach (var group in _storage)
5145
{
52-
_storage[group.Key ?? string.Empty] = group.Value.OrderBy(h => h.Order).Select(h => h.Value).ToArray();
46+
group.Value.Sort((a, b) => a.PreloadOrder.CompareTo(b.PreloadOrder));
5347
}
5448
}
5549

56-
private static (int order, string header) CreateHeader(StringBuilder headerBuilder, int relativeRootDistance, string url, IEnumerable<ResourceAssetProperty> properties)
50+
private static PreloadAsset CreateAsset(string url, IEnumerable<ResourceAssetProperty> properties)
5751
{
58-
headerBuilder.Clear();
59-
headerBuilder.Append('<');
60-
61-
for (int i = 0; i < relativeRootDistance; i++)
62-
{
63-
headerBuilder.Append("../");
64-
}
65-
66-
headerBuilder.Append(url);
67-
headerBuilder.Append('>');
68-
69-
int order = 0;
52+
var resourceAsset = new PreloadAsset(url);
7053
foreach (var property in properties)
7154
{
72-
if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase))
55+
if (property.Name.Equals("label", StringComparison.OrdinalIgnoreCase))
56+
{
57+
resourceAsset.Label = property.Value;
58+
}
59+
else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase))
60+
{
61+
resourceAsset.Integrity = property.Value;
62+
}
63+
else if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase))
64+
{
65+
resourceAsset.PreloadGroup = property.Value;
66+
}
67+
else if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase))
7368
{
74-
headerBuilder.Append("; rel=").Append(property.Value);
69+
resourceAsset.PreloadRel = property.Value;
7570
}
7671
else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase))
7772
{
78-
headerBuilder.Append("; as=").Append(property.Value);
73+
resourceAsset.PreloadAs = property.Value;
7974
}
8075
else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase))
8176
{
82-
headerBuilder.Append("; fetchpriority=").Append(property.Value);
77+
resourceAsset.PreloadPriority = property.Value;
8378
}
8479
else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase))
8580
{
86-
headerBuilder.Append("; crossorigin=").Append(property.Value);
87-
}
88-
else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase))
89-
{
90-
headerBuilder.Append("; integrity=\"").Append(property.Value).Append('"');
81+
resourceAsset.PreloadCrossorigin = property.Value;
9182
}
9283
else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase))
9384
{
94-
if (!int.TryParse(property.Value, out order))
85+
if (!int.TryParse(property.Value, out int order))
9586
{
9687
order = 0;
9788
}
89+
90+
resourceAsset.PreloadOrder = order;
9891
}
9992
}
10093

101-
return (order, headerBuilder.ToString());
94+
return resourceAsset;
10295
}
10396

104-
public bool TryGetLinkHeaders(string group, out StringValues linkHeaders)
105-
=> _storage.TryGetValue(group, out linkHeaders);
97+
public bool TryGetAssets(string group, out List<PreloadAsset> assets)
98+
=> _storage.TryGetValue(group, out assets);
99+
}
100+
101+
internal sealed class PreloadAsset(string url)
102+
{
103+
public string Url { get; } = url;
104+
public string? Label { get; set; }
105+
public string? Integrity { get; set; }
106+
public string? PreloadGroup { get; set; }
107+
public string? PreloadRel { get; set; }
108+
public string? PreloadAs { get; set; }
109+
public string? PreloadPriority { get; set; }
110+
public string? PreloadCrossorigin { get; set; }
111+
public int PreloadOrder { get; set; }
106112
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components.Endpoints;
5+
6+
internal class ResourcePreloadService
7+
{
8+
private Action<List<PreloadAsset>>? handler;
9+
10+
public void SetPreloadHook(Action<List<PreloadAsset>> handler)
11+
=> this.handler = handler;
12+
13+
public void Preload(List<PreloadAsset> assets)
14+
=> this.handler?.Invoke(assets);
15+
}

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
7373
services.AddSupplyValueFromPersistentComponentStateProvider();
7474
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
7575
services.TryAddScoped<WebAssemblySettingsEmitter>();
76+
services.TryAddScoped<ResourcePreloadService>();
7677

7778
services.TryAddScoped<ResourceCollectionProvider>();
7879
RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<ResourceCollectionProvider>(services, RenderMode.InteractiveWebAssembly);

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,6 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
278278
{
279279
if (_httpContext.RequestServices.GetRequiredService<WebAssemblySettingsEmitter>().TryGetSettingsOnce(out var settings))
280280
{
281-
if (marker.Type is ComponentMarker.WebAssemblyMarkerType)
282-
{
283-
// Preload WebAssembly assets when using WebAssembly (not Auto) mode
284-
AppendWebAssemblyPreloadHeaders();
285-
}
286-
287281
var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions);
288282
output.Write($"<!--Blazor-WebAssembly:{settingsJson}-->");
289283
}
@@ -320,15 +314,6 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
320314
}
321315
}
322316

323-
private void AppendWebAssemblyPreloadHeaders()
324-
{
325-
var preloads = _httpContext.GetEndpoint()?.Metadata.GetMetadata<ResourcePreloadCollection>();
326-
if (preloads != null && preloads.TryGetLinkHeaders("webassembly", out var linkHeaders))
327-
{
328-
_httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, linkHeaders);
329-
}
330-
}
331-
332317
private static bool IsProgressivelyEnhancedNavigation(HttpRequest request)
333318
{
334319
// For enhanced nav, the Blazor JS code controls the "accept" header precisely, so we can be very specific about the format

src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
77
using System.Globalization;
8+
using System.Net.Http;
89
using Microsoft.AspNetCore.Builder;
910
using Microsoft.AspNetCore.Components.Rendering;
1011
using Microsoft.AspNetCore.Components.Web;
@@ -28,6 +29,7 @@ internal class SSRRenderModeBoundary : IComponent
2829
private RenderHandle _renderHandle;
2930
private IReadOnlyDictionary<string, object?>? _latestParameters;
3031
private ComponentMarkerKey? _markerKey;
32+
private HttpContext _httpContext;
3133

3234
public IComponentRenderMode RenderMode { get; }
3335

@@ -38,6 +40,7 @@ public SSRRenderModeBoundary(
3840
{
3941
AssertRenderModeIsConfigured(httpContext, componentType, renderMode);
4042

43+
_httpContext = httpContext;
4144
_componentType = componentType;
4245
RenderMode = renderMode;
4346
_prerender = renderMode switch
@@ -106,6 +109,12 @@ public Task SetParametersAsync(ParameterView parameters)
106109

107110
ValidateParameters(_latestParameters);
108111

112+
// Preload WebAssembly assets when using WebAssembly (not Auto) mode
113+
if (RenderMode is InteractiveWebAssemblyRenderMode)
114+
{
115+
AppendWebAssemblyPreloadAssets();
116+
}
117+
109118
if (_prerender)
110119
{
111120
_renderHandle.Render(Prerender);
@@ -114,6 +123,16 @@ public Task SetParametersAsync(ParameterView parameters)
114123
return Task.CompletedTask;
115124
}
116125

126+
private void AppendWebAssemblyPreloadAssets()
127+
{
128+
var preloads = _httpContext.GetEndpoint()?.Metadata.GetMetadata<ResourcePreloadCollection>();
129+
if (preloads != null && preloads.TryGetAssets("webassembly", out var preloadAssets))
130+
{
131+
var service = _httpContext.RequestServices.GetRequiredService<ResourcePreloadService>();
132+
service.Preload(preloadAssets);
133+
}
134+
}
135+
117136
private void ValidateParameters(IReadOnlyDictionary<string, object?> latestParameters)
118137
{
119138
foreach (var (name, value) in latestParameters)

0 commit comments

Comments
 (0)