Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stencil 2.0 suggestion: improved async and error handling #2376

Open
cjorasch opened this issue Apr 16, 2020 · 2 comments · May be fixed by #2979
Open

Stencil 2.0 suggestion: improved async and error handling #2376

cjorasch opened this issue Apr 16, 2020 · 2 comments · May be fixed by #2979
Labels

Comments

@cjorasch
Copy link

Stencil version:

 @stencil/core@1.12.3

I'm submitting a:
[x] feature request

Summary:

The heart of Stencil is its ability to automatically segment code and load things dynamically. With a fast network and no errors this is a great experience for developers and end users and can provide high performance UI coupled with smart code loading.

It is easy, however, for developers to forget that async calls have the potential to be very slow and that errors can occur. Handling the cases where calls are slow or fail is very hard to do and requires handling at multiple places in the stack (Stencil, Ionic Core, and developer code).

Some examples in Ionic Core where slow performance can lead to problems include:

Rather than repeating all of the details here you can refer to these specific write-ups for more details about the issues that arise.

Part of the challenge is that Stencil makes it easy to do things that will not perform well on slow networks and it is extremely difficult to fully handle these situations without architecting it through all the layers (Stencil, Ionic Core and develolper code). The intent here is not to document how to make things faster but rather how to deal with things when they happen to be slow or fail.

Stencil 2.0 provides an opportunity for breaking changes that may not happen again for a while so it seems like a good time to look at potential improvements related to async loading and error handling.

Key areas for potential improvement include:

  • handling slow network latency
  • handling network errors
  • improved rendering approaches for components that display async data
  • hooks for ui cues related to in progress rendering
  • hooks for ui cues related to errors

This document attempts to provide an overview of some the issues and possible approaches vs. providing details about a single potential new feature. I considered splitting out smaller portions of the issue (sorry it is so long) but many things might be interrelated so I tried to create a broader view.

Handling slow network latency:

Consider the case of a Stencil based web application running on a desktop browser and using a slow network. A key issue that impacts the Stencil architecture is the latency even if the download speed is high. I recently had a user with a Comcast cable modem at 50mb download that was experiencing 400ms latency. That can have a huge impact if many round trips are required.

Running a Stencil based website with a simulated Slow 3G network in DevTools is an easy way to notice what happens with a slow network. If the code is not already on the machine then the user experience is pretty bad.

Part of the promise of Stencil is that it handles all the async loading so that you don't even need to think about it.

I created a relatively simple page in Stencil / Ionic Core that happened to use a number of nested components. Something like <my-page> renders <my-comp-a> which renders <my-comp-b> which renders <my-comp-c>. Nothing too fancy (all tiny components) and everything works great during dev.

So what happens when I try to display this page with a Slow 3G network (with 2s latency)?

1. click on a button to navigate to the page.  `<ion-nav>` is used to display the page.
2. `<my-page>` js file is loaded (takes 2s)
3. my-page import depencies are loaded (2s)
4. my-page.render() is called
5. `<my-comp-a>` js file is loaded (2s)
6. my-comp-a import dependencies are loaded (2s)
7. my-comp-a.render() is called
8. `<my-comp-b>` js file is loaded (2s)
9. my-comp-b import dependencies are loaded (2s)
10. my-comp-b.render() is called
11. `<my-comp-c>` js file is loaded (2s)
12. my-comp-c import dependencies are loaded (2s)
13. my-comp-c.render() is called
14. the page is displayed. At this point 16 seconds have elapsed

Yes this case is a bit contrived because components are likely to be in the cache, dependencies are likely to be loaded, etc. But it can and does happen (especially with complex design systems and not one person in control). And it doesn't even include async data requests that would add more time. I had an actual page in an app with lots of component nesting that worked fine during dev. It worked pretty well doing a refresh over the internet with a fast connection, but took 32 seconds with Slow 3G. So the promise of "auto-magic" loading is not quite enough for these situations.

So there are things that can be done to try to parallelize some of the component loading but this is fairly complicated for the developer and hard to handle all permutations of how components are used. Stencil actually does an extremely good job of pre-requesting downstream imports to reduce round trips for code dependencies. An observation here is that no matter what you do there are situations where rendering may be slow. Faster is always better but additionally it is important to be able to better handle situations where it is slow and be able to convey it to the user.

Rendering async data:

A common pattern for web applications is to display data coming from some data source that is not local. A typical implementation of this pattern is to have the component load the data using fetch or some other request mechanism and then display the results.

There are a number of potential issues with this approach, however. These include:

  • For any async operation the developer must get the data in both componentWillLoad and componentWillUpdate.
  • If the async data request is slow then nothing is rendered.
  • If there are any slow async calls in componentWillLoad or componentWillRender then not only does the component not render but rendering stops up the entire rendering stack. For example a page that includes many components and one of them loads data that happens to take a lot of time then the entire page does not render.
  • It is important for some components to render even if child components are in progress. For example, an application page should be able to at least display a back button if some of the child content is taking a long time to load. Otherwise the user just sees a blank page and can't do anything (or in the case of ion-nav has no idea that anything is happening).

Because of these issues I have taken the following approach for components that load data.

  • never load data in componentWillLoad to avoid blocking rendering
  • never load data in componentWillRender to avoid blocking rendering
  • keep data state and branch in render to handle appropriately
  • timeouts forcing update on async completion to update when state changes
  • this way all components render as quickly as possible (at least from a data standpoint)
  • it is a bit painful to do this in Stencil without defining a base class because there is lots of repeated code (even when using utility functions) that is hard to maintain
  • Stencil is in a great position to make this clean and easy

UI for in progress rendering:

It is sometimes important to indicate to the user that something is in progress and will be rendered later rather than jut being blank for slow operations. A typical approach is starting as blank, changing to progress indicator after some delay, changing to eventual result when done.

There are a number of things that could lead to an in progress state including

  • waiting for the component entry.js file to load
  • waiting for component import dependencies to load
  • waiting for async calculations including application data loading
  • waiting for child components to load

Advanced handling could potentially include

  • timeouts
  • ability to cancel loading (e.g. waiting for page to load and want to go back)

UI for errors:

Errors can come from a number of sources:

In general, the current behavior for things not caught and handled within developer code is:

  • components do not render anything
  • modals do nothing if component fails to load
  • navigation does nothing if component fails to load

Summary:

The intent of this list is not to complain, but rather to illustrate some of the challenges that developers face when building applications. Most frameworks suffer from similar challenges. Stencil is great but there are always things that can make it better. Good handling for async code and errors is an extremely difficult thing to handle well. Hopefully this list will trigger some ideas for future Stencil improvements.

There has been a transition from React style pure functional rendering (big js file and all sync) to Stencil style async code (large number of small js files and everything async). The optimal solution is probably somewhere in between.

Some ideas for improvements:

  • State information for components. Something like Loading / In Progress / OK / Error. This information would allow rendering logic to display appropriate ui depending on the state. Developer code would be able to alter state (indicate in progress or errors) to reflect things like external database calls. I am doing this currently for data components but it ends up being pretty coplicated to do well and awkward to generalize without being able to implement in a base class.
  • Support async render function. For example, render(): JSX | Promise<JSX>. This is somewhat supported by componentWillRender and componentDidRender but it seems like it would be simpler to handle it in a single method - especially if any data was being used. For example, error getting async data and wanting to render an error message. There would be no impact on existing code since sync result would still work and only components that needed the feature would use it. Async rendering is especially important if you want to call any methods on other components while rendering. Because they are async there is not way to do that now.
  • Support render arguments. For example, render(context?: RenderContext): JSX | Promise<JSX>. There is a lot of information that could be useful during rendering and simplify the process. Things like what caused the update (initial render / props changed / state changed / forceUpdate / etc.), the target element, the componenent state, etc. Using an optional param would avoid breaking existing code, avoid complicating components that don't need it, and help components that benefit from it. This might also simplify some of the current lifecycle methods so that fewer are needed.
  • Intelligent try/catch for all lifecycle methods. While every method could implement it's own try/catch handler in practice very few do this. Even within Ionic Core there is very little try/catch handling
  • Rendering fallbacks at higher levels. While some ui for errors and async state can be handled directly within a component, others can not be handled there. Things like loading errors (where the component js fails to load) can't use component rendering. Other cases may include parent components that do not want to show partial content while waiting for child rendering. This could be done like in react with a fallback lifecycle method. It also might be useful to have a global hook or event so the developer can take action.
@ionitron-bot ionitron-bot bot added the triage label Apr 16, 2020
@cjorasch
Copy link
Author

Here is an example of a library which attempts to improve upon React style JSX with some innovative async rendering. Not directly applicable to Stencil, but pretty interesting to see async rendering as a primary design driver.

https://crank.js.org

@tricki
Copy link
Contributor

tricki commented Jun 21, 2020

  • never load data in componentWillLoad to avoid blocking rendering
  • never load data in componentWillRender to avoid blocking rendering
  • timeouts forcing update on async completion to update when state changes

Loading data in componentWillLoad will not block rendering as long as you don't return a Promise or use async componentWillLoad (same for willUpdate). This way you don't need to use timeouts. I usually load the data from an async method which I call in willLoad (without await) and in the render method I check if this.data is:

  • null => show loader
  • an empty array => render "no data"
  • a non-empty array => render the data

You might want to return a Promise while prerendering though so the prerendered version includes the data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants