Script Queue
Loading JS from Block Components with dependency-aware ordering
Script Queue β Loading JS from Block Components
The Problem
uTPro uses a deferred loading strategy β jQuery and plugins only load after user interaction (or 3s idle) to improve Core Web Vitals. This creates a challenge:
If a block component renders its own <script defer> tag, it may execute before jQuery is available β causing $ is not defined errors.
Timeline:
ββ HTML parsed β component <script defer> executes immediately β jQuery not loaded yet
ββ User interacts (or 3s idle)
ββ jQuery + plugins start loadingThe Solution: Script Queue
Components register their JS file paths into a per-request queue. The Layout then renders them in the correct position β either inside the deferred loader (after jQuery) or as standalone <script defer> tags.
Component.cshtml β Html.QueueScript() or Html.QueueStandaloneScript()
β
HttpContext.Items (per-request, dedup)
β
_Layout.cshtml β @Html.RenderScriptQueue() β appends to deferred array
β @Html.RenderStandaloneScripts() β renders <script defer> tagsTwo Methods
| Method | Use when | How it loads |
|---|---|---|
Html.QueueScript(path) | Script needs jQuery or other deferred libraries | Appended to sequential deferred loader (loads after jQuery) |
Html.QueueStandaloneScript(path) | Script is vanilla JS, no dependencies | Rendered as <script defer> (loads independently, faster) |
Both methods deduplicate automatically β same script registered twice = rendered once.
Usage in Component
@if (Context.Request.IsBlockPreviewRequest())
{
@* Preview mode: CSS only, no JS needed *@
<link rel="stylesheet" href="~/css/@CurrentSite.GetItem().Root.Name/Components/MyComponent.css" />
}
else
{
@using (Html.SetSection(Model.GetType().ToString(), HtmlSectionBlockExtensions.Position.BodyBottom))
{
<link rel="stylesheet" href="~/css/@CurrentSite.GetItem().Root.Name/Components/MyComponent.css" />
}
// QueueStandaloneScript: vanilla JS, no dependencies, loads via <script defer>
// QueueScript: needs jQuery, appended to deferred loader chain (loads AFTER jQuery)
Html.QueueStandaloneScript($"/scripts/{CurrentSite.GetItem().Root.Name}/my-component.js");
}Example: Script that needs jQuery
Html.QueueScript($"/scripts/{CurrentSite.GetItem().Root.Name}/slider.js");Example: Standalone script (no jQuery)
Html.QueueStandaloneScript($"/scripts/{CurrentSite.GetItem().Root.Name}/cookie-consent-banner.js");What Happens in the Layout
<!-- Deferred loader: jQuery + plugins + QueueScript entries -->
<script>
(function(){
var scripts=[
'/assets/scripts/jquery.min.js',
'/assets/scripts/jquery.dropotron.min.js',
'/scripts/uTPro/util.js',
'/scripts/uTPro/main.js'
,'/scripts/uTPro/slider.js' // from QueueScript
];
// loads sequentially after user interaction or 3s idle
})();
</script>
<!-- Standalone scripts: QueueStandaloneScript entries -->
<script src="/scripts/uTPro/cookie-consent-banner.js" defer></script>Rules
- Vanilla JS (no dependencies) β
QueueStandaloneScript - Needs jQuery or any library in the deferred chain β
QueueScript - Never put
<script src="...">directly in a component β use the queue - CSS can still use
SetSectionas before (no dependency ordering issues) - Both methods are safe to call multiple times with the same path β dedup is automatic
Architecture
Implemented in uTPro.Extension/ScriptQueueExtensions.cs:
- Uses
HttpContext.Itemsfor per-request storage (zero serialization cost) HashSet<string>for O(1) dedup- HTML-encodes paths to prevent XSS
- Two separate queues (deferred vs standalone) with independent storage keys
| File | Role |
|---|---|
uTPro.Extension/ScriptQueueExtensions.cs | Extension methods (QueueScript, QueueStandaloneScript, RenderScriptQueue, RenderStandaloneScripts) |
Views/uTPro/_Layout.cshtml | Calls RenderScriptQueue() inside deferred array + RenderStandaloneScripts() after |
Views/globalLayout.cshtml | Calls RenderStandaloneScripts() (no deferred loader in fallback layout) |
| Component .cshtml files | Call Html.QueueScript() or Html.QueueStandaloneScript() |