RFC: Reconciliation of head elements #446
triskweline
started this conversation in
Ideas
Replies: 2 comments 1 reply
-
Changes from the section Incremental component definitions are already implemented in Unpoly 3. |
Beta Was this translation helpful? Give feedback.
0 replies
-
After a draft implementation and some internal discussions we decided that we are not going to ship the RFC in this form. Many aspects turned out to be impractical or unnecessary, in particular:
The team still cares about:
The current plan is:
|
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
This RFC proposes the creation of a new
up.asset
namespace that manages the inclusion of scripts, styles and other elements in the<head>
.This document was born from discussion #296.
Your feedback in the comments is appreciated!
Motivation
<meta>
and<link>
elements in the<head>
updated as the user navigates between pages.Design goals
Non-goals
To reach our design goals we are not going to care about any of the following:
up:location:changed
event for that (example).Types of elements in the
<head>
A naive approach would simply replace the current
<head>
with the<head>
from a response. This approach has many issues:<title>
or<link rel="canonical">
being updated when we render a minor fragment (like a message counter).In reality the
<head>
elements hosts a wide variety of elements, each with their own constraints governing when they should be added or removed from the page.Here is an example to illustrate why a one-size-fits-all approach cannot work:
Finding meta properties
Unpoly matches meta properties through a configurable array of selectors. The default configuration is:
These are all assumed to be in the
<head>
. We don't support merging of meta properties outside of the<head>
.Finding scripts
We also have a selector for what we consider "scripts":
Finding styles
We also have a selector for what we consider "styles":
Reconciliation Strategies
To cover various styles of managing scripts, styles and meta properties, Unpoly will offer various strategies that control how elements are added to and removed from the head:
keep
<head>
merge
merge-if-history
merge
, but only applied when updating historyswap
<head>
with response<head>
swap-if-history
swap
, but only applied when updating historyEach strategy is explained in detail below.
Default strategies
The default strategy can be configured for each type of
<head>
element. The defaults we will ship with are:Note how we're defaulting to not change scripts and styles by default. This is a safe default as the
<head>
will not generally be safe for reconciliation. We want this change to be backwards compatible.Overriding defaults for individual render passes
Links or forms can override the strategy like this:
JavaScript calls can override the strategy like this:
Merge strategy
The
merge
strategy is always additive:<head>
Pairing elements
The identity of a meta prop is its derived target. A pair of meta properties with the same identity may differ in other HTML attributes.
The identity of a tracked stylesheet is its URLs (
[href]
attributes) without its version.The identity of a tracked scripts is its URLs (
[src]
attributes) without its version.Untracked scripts or stylesheets have no identity and are never considered equal. This will cause them to be inserted multiple time with the
merge
strategy.What elements are considered "active"
Meta properties are considered active when they are currently on the page.
Tracked stylesheets are considered active when they are currently on the page.
Tracked scripts are considered active if they've ever been on the page. They are still considered active after removal from the page. This is because we cannot "unload" a script once it has executed.
Synchronization
Tracked assets compare their version. If the version differs, we emit
up:asset:revision
. Even if the version differs, we never update the element.Meta properties on the page update their attributes to match the attributes from the response element. If possible, avoid re-inserting an identical element to prevent unnecessary requests and layout reflow.
Swap strategy
The
swap
strategy is similiar to themerge
strategy, with one exception:Ths
swap
strategy is for removing meta properties and styles that are no longer needed. A use case is flipping between sections that have completely different styles, e.g. customer-facing frontend and admin-facing backend areas.Keep strategy
We don't change elements in the
<head>
, regardless of the response.Handling responses without a
<head>
If the response does not contain a
<head>
, only thekeep
strategy is applicable.In particular the
swap
andmerge
strategies doe not change elements when the response is missing a<head>
.Only processing for history changes
There are two strategy variants
merge-if-history
andswap-if-history
. These behave likemerge
andswap
respectively, but are only applicable if the current render pass updates the browser history.The
swap-if-history
strategy is the default for updating meta properties. Since all information in meta properties is related to the current history entry, we only want to update them when we're changing history. We don't want to update them when we render a minor fragment, like a message counter.Attempting multiple strategies
The user may configure multiple strategies like this:
The first applicable strategy will be used.
Incremental component definitions
We want to better support a server-side pattern where every response only includes assets for components used on this one page.
Here is an example response for page that uses a "WYSIWYG" and "chat" component:
When you manage your asset this way, this would be the ideal configuration for you:
Allowing incremental compiler definitions
To support incremental loading patterns, we need to change the way
up.compilers()
handles late definitions of compiler functions. Currently, when compilers are registered late, they only apply to future render passes. Unpoly logs a warning to that effect:This behavior does not work for the example above. I do not need compilers to run for future render passes, they need to run for this render pass. Also see issue #302.
We should change this behavior so, when compilers are registered late, all layers are re-compiled with only those new compilers.
Limitations
We cannot support late definition of compilers that have a priority. We also cannot support macros, which are essentially compilers with a very high priority. Since we cannot re-compile existing elements, priorities cannot honored.
In these cases we keep the existing behavior: Print a warning and run the compiler for future render passes:
Assets in the
<body>
We want to keep supporting apps that include scripts or styles within the fragments that need them:
These assets are included unless they are tracked and already active.
Processing of assets in the
<body>
can be disabled:Removal
Stylesheets in the
<body>
are removed when their container fragment leaves the DOM.Script elements in the
<body>
are also removed with their container fragments, but since the script has already executed this has no effect other than cleaning up the DOM.Reusing fragments within the same page
We must cover the case where the same tracked asset appears in a single fragment update:
In this case the tracked asset must only be inserted once.
Preventing duplicate processing
When a script or stylesheet (together "asset") are tracked, we only run them once.
By default we track all assets loaded from a remote URL, as they are most likely libraries or component definitions:
Users can opt out using an
[up-track=false]
. This causes the script/stylesheet to be re-included when merging.This can be configured:
Tracking inline scripts
We do not by default track inline scripts (
<script>javascript</script>
) as these are often used to initialize elements on the current screen, and must be re-run on subsequent render passes:Inline scripts can opt into tracking by setting an
[up-track]
attribute:Since we cannot parse the identity of an inline script from its
[src]
URL, the user must also set an[up-id]
or[id]
attribute for the script to be trackable.The version of a tracked inline script is its text content.
Tracking internal styles
We do not by default track internal stylesheets (
<style>css<style>
).As internal stylesheets are not used very often, and there are no discernable patterns across usages. The safer default is to not have magic rules.
Internal styles can opt into tracking by setting an
[up-track]
attribute:Since we cannot parse the identity of an internal script from its
[href]
URL, the user must also set an[up-id]
or[id]
attribute for the stylesheet to be trackable.The version of a tracked inline stylesheet is its text content.
Asset versions
Build tools like Webpack or esbuild include a content hash ("version") in the filename:
This allows servers to cache assets with long expiry times. If the underlying asset changes, it will produce a different filename (and hence a separate cache entry):
When the URLs of two assets only differ in their version, we must consider them the same asset for the purpose of
<head>
merging.Format
By default we we support asset URLs in the following formats:
/assets/application.js
(no version)/assets/application-c42ce512.js
(esbuild examples style)/assets/application.c42ce512.js
(webpack examples style)We add a configurable function that parses asset URLs:
Assets without a version always return the version
"1"
:The URL is normalized so we get the same identity for a path and a fully qualified URL pointing to the same resource:
If users are using different fingerprint schemes than
id.version.ext
orid-version.ext
, they can setup.asset.config.parsePath
to a new function.Handling changed assets
While we don't re-process tracked assets, they may change between requests. This happens when the developer deploys a new app version with changes to its asset code.
The difference may be minor or major. We cannot know if it's safe to continue running the existing page with old assets.
Scripts are why handling this is hard. Since we cannot "unload" scripts, we cannot update existing scripts with a new version. Turbolinks / Turbo handles this by automatically reloading the entire page when a changed script is detected. This default seems too crude, as unsaved user state may be discarded. Some apps may rather want to show a notification: "A newer version of your app is available. [Reload]", and wait until the user is ready to reload.
Hence this proposal for handling changed assets:
up:asset:revision
). The event should contain references to the old and new asset element.E.g. a developer configure a Turbo-like default (reload on change):
Or the developer could ask the user:
Or the developer could decide that it's OK to update changed stylesheets, but leave scripts unchanged:
Existing title handling
There is existing handling of
<title>
, which we need to keep supporting.A
{ title }
option orX-Up-Title
response header overrides the<title>
element in a response.Layer considerations
Unpoly has layers. However the page only has a single
<head>
with a global namespace for meta properties, style rules and JavaScript state.Meta properties
Layers may or may not be configured to update the history.
If a layer does update history, it should also update meta properties when it opens, using the reconciliation strategy configured in
up.asset.config.headMeta
. When it closes, the meta properties of its parent layer must be restored.Example:
<meta prop="og:image" content="root.jpg">
<meta prop="og:image" content="overlay.jpg">
<head>
to<meta prop="og:image" content="overlay.jpg">
while the overlay is open.<meta prop="og:image" content="root.jpg">
Scripts
When we open a layer, the reconciliation strategy configured in
up.asset.config.headScripts
is applied.There is no special handling of scripts when the layer closes.
Styles
When we open a layer, the reconciliation strategy configured in
up.asset.config.headStyles
is applied.There is no special handling of styles when the layer closes.
Existing solutions
To solve similiar problems in Unpoly 2.x and 3.x, developers are using the following patterns:
Meta properties
Unpoly already merges the
<title>
when rendering with history.For other elements users often use
[up-hungry]
:Unpoly 3 improves this by no longer needing the
[id]
property. It also gives us[up-if-target]
to only update when the main target is rendered:These workarounds work, but have some minir drawbacks:
[up-hungry]
in their<head>
.[up-if-target=":main"]
when they really mean "when we're pushing a history entry".We could offer a better default with less manual configuration.
Scripts
See Unpoly: Loading large libraries on-demand for a way to split a large bundle into multiple files.
The drawback here is that all compilers need to be defined up-front, even if they're just a shell to load in more code. See issue #302.
Styles
One way to load in additional styles later is to place the style element within the swapped fragment.
You can also define a hungry section in your application layout:
Both solutions are non-additive, meaning the styles will be unloaded when the fragment leaves the DOM or is updated with new HTML.
Prior art
Other libraries have worked on the same problem:
[data-turbo-track]
attributehead-support
extensionBeta Was this translation helpful? Give feedback.
All reactions