[Complete] RFC: Deferred Loading #50716
Replies: 38 comments 71 replies
-
Checked before the end of ng-conf keynote :D So awesome! |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
@jessicajaniuk is this feature just going to be using |
Beta Was this translation helpful? Give feedback.
-
1B: Signal |
Beta Was this translation helpful? Give feedback.
-
Would this work only with standalone components and features? That would be my guess since we could lazy-load anything using |
Beta Was this translation helpful? Give feedback.
-
That will move Angular so much forward! It opens a lot of opportunities that were not possible before.
If it will be possible to catch using Groundbreaking move! |
Beta Was this translation helpful? Give feedback.
-
That's huge news! I'm super hyped! My question is, when would the {:error} block triggers exactly? How can a component fails to render? |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Discussion Question 1B: Yes in order to to control the order of deferred loads. Discussion Question 3B: Developers are looking for optimising Google Web Vitals https://web.dev/vitals/ and this could be a tool to address the LCP of an application by reducing main bundle size. However, how is the order of multiple deferred blocks in terms of the download process of the chunks? Imagine we have 6 different deferred templates above the fold using different libraries => creating different chunks. We want to control the deferred templates and prioritise the load of one above the other. Is there any control about the bundling and order of deferred blocks planned? I am also on ng-conf2023 so happy to answer or discuss details in person :) |
Beta Was this translation helpful? Give feedback.
-
This looks great! It reminds me of Astro's client directives like Can we consider adding support for a trigger for
|
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Regarding the semantics of
But I don't know if Edit: I suggest these variants:
|
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Thank you for this RFC, Anything that improves web core vital is always appreciated. I am wondering how would dynamically created components be handled? Any thoughts ? |
Beta Was this translation helpful? Give feedback.
-
I can think of two triggers. One waits for the data to show up. Instead of delaying the rendering, it would delay the fetching of the components. A second is a depends on trigger, controlling the order that components are loaded and rendered. |
Beta Was this translation helpful? Give feedback.
-
What would happens in that scenario? <p>hello world!</p>
{#defer}
<calendar-cmp />
{/defer}
{#defer}
<other-cmp />
{/defer} When the browser is idle, it will fetch the calendar then when calendar chunk has finished downloading it will fetch other-cmp? |
Beta Was this translation helpful? Give feedback.
-
One of the things that I didn’t see and should go in parallel (I believe) with the built-in control for consistency is the fact that the syntax is {#defer} I believe this should go hand by hand with what the built-in control syntax ends up adopting (example [#if ] ). |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
So I guess this isn't meant to work with lazy loading data, only code? It seems like it would be confusing not to work with both. Also, what about animations, will placeholder etc support out animations? |
Beta Was this translation helpful? Give feedback.
-
Great, innovative functionality. Good job AngularTeam. I just have a question about this part: "Wrapping content with no dependencies Aside from performance considerations, I see a lot of potential in cases where there are no dependencies. Instead of writing a whole script using IntersectionObserver to load a DOM structure when it becomes visible, I would prefer to use a simple "defer" with an "on viewport" option to nicely display an HTML fragment with some animation. I'm not insisting, and I understand that wasn't the purpose of adding this feature. I just feel that even in these simpler cases, I might want to take advantage of it. Warm regards to everyone, and thank you so much Angular for making it a pleasure to work with you every day :) |
Beta Was this translation helpful? Give feedback.
-
The Testing StoryGiven that testing is mainly achieved using JIT, what will be the behavior in that case? It would be nice to have a testing API that allows us to manually control the state of the |
Beta Was this translation helpful? Give feedback.
-
I think that |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Great work. 🥇 1B: Yes. It would help manage and control the applications. |
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Impact on Core Web Vitals, especially CLSJust a thought, incremental loading of deferred components might have a negative impact on web vitals, especially Cumulative Layout Shift (CLS). Best benefits are achieved in combination with SSR, as the goals mention. I feel that alongside this new tooling, guidelines and best practices should also be provided to avoid common issues. How to do proper skeleton loading would be great with real examples. |
Beta Was this translation helpful? Give feedback.
-
Caching with Service WorkerFrom the several techniques for deferred loading mentioned in the RFC, I assume that they are happening in the main thread. Since in many cases the goal is immediate interaction, using Service Workers would help. Qwik does so by eagerly caching those scripts with a SW, so that when the main thread needs them there are already available, not waiting to be downloaded. This could be run in parallel while the main thread renders, no need to wait until idle or visible. Are there any plans on providing a combined solution that could integrate well with SW for pre-fetching? Reference: https://qwik.builder.io/docs/advanced/speculative-module-fetching/ |
Beta Was this translation helpful? Give feedback.
-
Lazy data loading, async providersLazy loading components is great, but in many cases they need data along, one cannot live without the other. Questions:
|
Beta Was this translation helpful? Give feedback.
{{title}}
{{editor}}'s edit
{{editor}}'s edit
-
Authors: @AndrewKushnir, @clydin, @jessicajaniuk
Area: Angular Framework
Posted: June 14, 2023
Status: Open
The modern web emphasizes delivering a strong user experience during the loading of an application. Metrics like Core Web Vitals quantify this experience, highlighting the performance of the initial load.
One standard and effective technique for optimizing this loading experience is to defer lower priority parts of the UI and focus resources on loading the most critical parts of the page. For example, a page which shows a main video and a list of comments may be optimized to load the video first, and defer loading the code to render the comments until the video is fully buffered and ready to play.
Angular supports lazy loading of parts of the application via the Router (lazy routes). Lazy loading of individual components is achievable via dynamic
import()
andngComponentOutlet
, but this approach can be complex and error prone.As a result, we see a lot of potential in introducing an ergonomic, holistic approach to deferred loading in the core framework, that works across both client- and server-side rendering. This RFC proposes a new framework primitive:
{#defer}
.A note on the timeline
We're conducting this RFC as a part of our larger efforts around initial rendering and server-side rendering. While we're excited by the ideas presented here, our goal is to gather some early feedback to inform our roadmap, and we may not progress to the implementation phase until other projects have completed. When we do begin implementation, features will land incrementally over time.
What kind of feedback are we looking for?
While we're excited to hear any and all comments on this primitive for deferred loading in Angular, we have additionally called out specific discussion points throughout the RFC for directed feedback. When you leave a comment, be sure to note the open question(s) to which you're responding.
As always, keep our code of conduct in mind. We know that a proposal of this scope significantly impacts Angular and the way you use it in your projects. We appreciate your commitment and investment in this project and ask to keep comments respectful.
Goals
8 Support deferred loading of directives & pipes as well as components
#defer
Deferred blocks
(Note: This deferred loading primitive makes use of the new block template syntax proposed in the Control Flow RFC -- check out that RFC for more details)
The primitive is represented in template by the following block group:
{#defer}...{/defer}
. Dependencies (components, directives, etc) referenced within the deferred block are loaded lazily. As a result, deferred blocks are always rendered asynchronously. This includes all dependencies within the deferred block, which would include components, directives, and pipes used within those dependencies.on
andwhen
Conceptually the defer block is a construct which, when triggered, replaces placeholder content with lazily loaded content. Developers have two options for configuring when this swap is triggered:
on
andwhen
.when
specifies an imperative condition in the form of an expression that returns a boolean. When this expression becomes truthy, the placeholder is swapped with the lazily loaded content (which may be an asynchronous operation if the dependencies need to be fetched).Crucially, if the
when
condition switches back to false, the defer block is not reverted back to the placeholder. The swap is a one-time operation. If the content within the block should be conditionally rendered, an if condition can be used within the block itself.on
specifies a declarative trigger condition, usually some kind of event. We will have a series of predefined event triggers (see the Trigger Types section). An example would beon interaction
oron viewport
.Multiple event triggers would be supported at once. For example,
on interaction, timer(5s)
means that the defer block will be triggered if the user interacts with the placeholder, or after 5 seconds.A more full example is below
Alternatively you could use both
when
andon
together in one statement, and the swap will be triggered if either condition is met.:placeholder
blocksBy default, defer blocks do not render any content before they're triggered. Using a
:placeholder
block, developers can specify which content should be shown before the defer block is triggered.This block may have any content including DOM nodes, components, directives, pipes, etc.
Note: dependencies of the
:placeholder
block are eagerly loaded and not deferred.The
:placeholder
block is optional, but certain on conditions will require a non-empty placeholder. Additionally, you can specify aminimum
condition, which takes a duration (i.e. 50ms). This allows users to specify a minimum duration of time to show the placeholder template before switching away to the next template. This allows users to prevent a fast flickering that could occur if the triggering state was immediate and the response only took a small amount of time.:loading
blocksThe
:loading
block defines content that should be displayed while the defer block is fetching dependencies in order to render its defer template. Similarly to the:placeholder
block above, this block is optional and if not provided, defer blocks will continue to show the:placeholder
(if any) until the defer content is loaded.Note: similarly to
:placeholder
, dependencies of the:loading
block are eagerly loaded.When using the
:loading
block, you can also specify anafter
condition, which takes a duration (i.e. 50ms). This allows users to specify a minimum duration of time to wait before showing the loading template. The loading template would not be shown if the request to deferred load took less time than the specified duration. This is to prevent layout thrashing caused by a fast flickering between the loading template and defer template.Similar to the
:placeholder
, you can specify aminimum
condition, which also takes a duration (i.e. 50ms). This allows users to specify a minimum duration of time to show the loading template before switching to the content. This allows users to prevent a similar fast flickering that could occur if the loading only took a small amount of time or a short duration after the after condition finished.:error
blocksThe error block represents a UI that should be rendered when deferred loading fails. Similar to the
placeholder
andloading
blocks above, this block is optional and if not provided, defer blocks would not render any content.Note: similarly to
:placeholder
and:loading
, dependencies of the:error
block are eagerly loaded.Along with the error block, a user will have access to the error thrown during the loading process via a
$error
variable accessible in the error block.Discussion Question 1B
: Do you see the need to be able to observe when the state of a defer block changes via a signal or event?Resource Prefetching and Content Rendering
There may be a desire to separate the deferred loading action from the content rendering action. For example, users may want to prefetch a set of dependencies in preparation for an anticipated user action, reducing the delay to that deferred block being interactable. In that case, users can optionally provide a prefetch condition to prefetch dependencies before the main trigger. prefetch syntax works similarly to the main defer condition, and accepts when and/or on to declare the trigger.
In this case, when and on associated with defer controls when to render, and prefetch when and prefetch on controls when to fetch the resources. This enables more advanced behaviors, such as letting you start to prefetch resources before a user has actually seen or interacted with a defer block, but might interact with it soon, making the resources available faster.
Just like with on and when with defer, these are one way behaviors. Switching prefetch when to false does not hide the content. Hiding content can be accomplished when composed with if.
We plan to take into consideration the current browser configuration and the user's connection speed by looking at the browser's navigator.connection data, NetworkInformation.saveData, and NetworkInformation.effectiveType when supported by the browser. This will ensure we provide the best possible user experience around prefetching.
Timeouts
Defer blocks provide an API for developers to configure timeouts for deferred loading operations, after which the request should be considered failed.
To configure this behavior, timeout can be defined on the
{#defer}
block, which defines the timeout duration in milliseconds:Discussion Question 2B
: Are there use cases where different deferred blocks in an application should use different timeouts?Trigger Types
idle
idle would trigger the deferred loading once the browser has reached an idle state. This is the default behavior with a defer block.
interaction
interaction means click, focus, touch and input events (keydown, blur, etc).
immediate
immediate triggers the deferred load immediately, meaning once the client has finished rendering, the defer chunk would then start fetching right away.
timer(x)
timer(x) would trigger after a specified duration.
hover
hover would trigger deferred loading when the mouse has hovered over a trigger area, which could be the placeholder content or a passed in element reference.
viewport
viewport would trigger the deferred block when the specified content enters the viewport using the IntersectionObserver API. This could be the placeholder content or an element reference.
Example #1
: deferred loading is triggered when placeholder content becomes visible in a viewport.Example #2
: deferred loading is triggered when another element becomes visible in a viewport using a local ref:These triggers are designed to handle the most common scenarios. If your use case requires a more complex scenario than the predefined trigger list, use when to define your condition.
Discussion Question 3B
: Is this list of predefined triggers sufficient to handle the majority of cases that users may have? Can you think of other common trigger types that we didn't account for in this design?Discussion Question 4B
: Can you think of use cases where you would need to change the default trigger behavior globally for all {#defer} blocks in your application?Nesting
{#defer}
blocks can be nested and there are legit use-cases for doing this. For example: you may want to render a big component when it becomes visible in a viewport, but load some inner components once a user clicks a button.Important note: nesting of defer sections may cause performance issues where components would sequentially load extra resources as Angular progresses with the rendering and discovers more defer boundaries. We plan to provide diagnostics and tooling for developers to help avoid this issue. See "Cascading Requests" section below.
Server Side Rendering Behavior
When rendering an application on the server, defer blocks always render their placeholder (or nothing if a placeholder is not specified). Triggers are ignored on the server.
See the next section for more information on the future plans for integration of defer with server side rendering.
Potential Future Path to Partial Hydration
Our roadmap for server-side rendering includes research into expanding our hydration implementation to support partial hydration - leaving server-side rendered content dehydrated after the initial page load. defer could be extended to integrate such functionality.
With partial hydration enabled, deferred content could be server-rendered and then left dehydrated until triggering conditions are met. We plan on conducting more research in this area and will follow up with a future RFC potentially bringing extra configuration to the defer block that would allow you to configure server rendering and hydration behavior in a more granular way.
Common Pitfalls
Cascading Requests
There are cases where nesting multiple defer blocks may cause cascading requests. An example of this would be when an defer block with an immediate trigger has a nested defer block with another immediate trigger. If it's all within one template, we could easily group those into one request, but in this case in a nested component template, this would not be catchable. We plan to implement extra diagnostics to warn developers about such patterns:
For nested defer blocks within a single template, the compiler can detect those patterns and output a warning to a user. This can also be handled by the Extended Template Diagnostics subsystem.
For nested blocks across multiple components, we can add detection mechanisms to runtime (in dev mode only).
This would also be highlighted in the docs as an anti-pattern (along with other good and bad practices).
Content Projection inside Defer Blocks
Dependencies of the content projected from outside of a component are not deferred when projected into a defer block. If you want to deferred load projected content dependencies, you can wrap that content into {#defer} block (with the necessary conditions) separately.
Wrapping content with no dependencies
If a user writes a {#defer} block, and inside that defer block is only content that has no dependencies (components, directives, pipes, etc) to extract, the defer block actually serves no purpose. We'll want to catch this case and provide an error to the user. The prior case of content projection inside of a defer block is one example. If a user only places DOM inside, and there are no directives applied to any of them, that would be another case. We'll highlight in the docs that this is an unnecessary use of defer blocks. We'll also want to add compile time checks for this to prevent users from making this mistake.
Cumulative Layout Shift (CLS) Risk
When using
{#defer}
blocks above the fold, if placeholders do not match the exact size of the deferred content that will replace it, it will contribute to the cumulative layout shift of the application's core web vitals. From a strictly CWV perspective, the recommendation is generally that you should only insert content a) out of viewport, or, b) in response to user input.Frequently Asked Questions
Q: Why is this feature called defer?
We've deferred answering this until later on in the FAQ.
Q: Why is this feature called defer?
The action of what this feature is doing is deferring the loading of something until a certain condition is met after the initial page load. This is a behavior that's similar to the script defer attribute, which loads resources in parallel and executes after the page has been parsed. Lazy loading is loading only when needing to use it. Deferred loading can be used for more than just loading something lazily.
Q: Can I use in a deferred block?
Yes, with the caveat that it is not the only thing inside of the defer block. See the section above on wrapping content with no dependencies for more information.
Q: Can I imperatively trigger deferred loading?
You can use a when expression to trigger the deferred block at any point.
Q: Is there a way to observe when defer blocks are loaded or state has changed?
See discussion question 1B.
Q: Can I define my own triggers?
No, we don't plan on supporting user-defined triggers. The current set of declarative triggers will be tightly integrated into the framework. If you have ideas for new triggers that would be useful in your applications, we'd love to hear them (see discussion question 4B).
You can also use a when expression to completely customize the triggering logic.
Beta Was this translation helpful? Give feedback.
All reactions