[MUSIC PLAYING]
JUSTIN FAGNANI: So my
name is Justin Fagnani.
And I work in Chrome on things
like Web Components, polymer,
and lit-html.
And right now, I'm going to talk
about the virtue of laziness.
Next slide.
Advance.
Today is the day.
That happened.
Yeah, OK.
So we're going to look at how
to do less, be lazy, and take
breaks, and end up with a
better web application for it.
And when I say
better, I'm really
talking about four
overlapping goals here.
We want to deliver
great user experiences.
And we want our apps to be
fast and to respond quickly
to input and changes.
And more than just
making fast apps
or making it possible
to build fast apps,
we want to make
this easy, so easy
hopefully that it's the
default. Because this
is going to make our users
happy, our developers happy.
And happy developers will
make better user experiences
in the long run.
So with these four
general goals in mind,
I'm going to walk through
several techniques
for leveraging asynchronous
programming for building better
UIs.
So we're going to
look at batching work
for better performance
and developer experience,
keeping our UIs responsive
with non-blocking rendering,
managing async state
for a better user
experience and developer
experience, and then finally,
coordinating async UIs once we
have all this asynchronicity
throughout our application.
And this makes for a handy
little talk outline right here.
So I'm going to
give some background
and then jump right into it.
But first, a quick note.
This is day two
of the Dev Summit
so it's a little more
future forward looking.
And the stuff I've made here,
the demos and helper code
is a little bit experimental.
But it's only using
current browser features,
and so all of these
techniques still work today.
So now for a little background.
I mentioned that I work on
web components and lit-html,
so we're going to use
these things as the basis
for the demos in the talk.
So if you haven't used
web components before,
Web Components lets you define
your own HTML element tags.
So it really refers
to two specs here,
custom elements and Shadow DOM.
And combined, they let
you define your own tags
with custom
implementation and UI.
So to create a custom
element, you simply
extend from HTML element,
a built in class,
you add your
implementation, and then you
register your class
with a specific tag
name with the browser.
And from there, you can use
this element and that tag
name anywhere you can use HTML,
so in your main page document,
inner HTML, document
create element,
even in other frameworks.
So next, lit-html,
lit-html is a way
to write declarative HTML
templates in JavaScript.
And we use tagged template
literals to write them.
This is a feature
that came out in ES 6.
And these are strings that are
denoted with backticks instead
of quotes, and they can have a
template tag in front of them.
And we're going to use the
lit-html template tag here,
which is just going to allow
us to process this template
to make it more efficient.
And then inside of our template,
we can have expressions.
And these are just plain
JavaScript expressions.
Once you have a template, if you
want to render it to the DOM,
you simply pass it to the
lit-html render function
and give it a node to render to.
And it's going to make
that DOM appear there.
And the nice thing is that if
you call this render function
multiple times at the same
template but different data,
lit-html is going to
take care to only update
the expressions that changed.
It will never update the rest
of the DOM in the template.
And then finally, if
you take web components
and lit-html and
combine them together,
you end up with LitElement.
So LitElement is a convenient
way to write web components.
Because this is day two and a
little more forward looking,
I'm using some future
JavaScript features here
like decorators
and class fields.
And LitElement really
gives you two features.
One is the ability to declare
observable properties.
So these decorators
here are going
to create getters and setters
instead of a field here.
And the setters are
going to recognize
when this property changes
and then tell the element
that it needs to update.
The other feature
is that it lets
you write a render method that
returns a lit-html result.
And so when the element knows
that it needs to update,
it's going to call this
render method, take the result
and render it to the shadow
root of this custom element.
And then finally, we give
you a little helper here,
so you can use a decorator
to register the element.
So once you do that, and
you create your element,
then you can use it
anywhere you would HTML,
and it will render
as you expect.
So that brings us to
our first technique
here, which is batching work.
And if we go back to
our element definition,
you'll see that in
the render method here
this is called for us
automatically by the LitElement
base class.
But the question
that comes up here
is, when is this method called?
So to look at that, let's take
a look at a little example here.
We're going to use this
element imperatively,
but this also applies if
you used it in the main HTML
or if you used it
with a framework.
So we're going to create
an element instance,
and then we're going
to set a property.
So the question is,
should we render now?
We could render now.
But we don't know
that we're not going
to set another
property right after we
set this property here.
And if we did render
after every property set,
we would be rendering
multiple times potentially
for every element as
we update the data.
We don't want to do that.
So instead, we're going
to enqueue a task.
And then, in the
future, that task
is going to run and actually
render that element.
And so that we know when
the element has rendered
and when it's complete, we
add this promise hanging off
the element here
called update complete.
And this is going to resolve
when the element has rendered.
And if you wait for
it, you know that you
have a fully rendered element.
And the way that
this works is we
have an asynchronous
update pipeline
under the hood in the element.
So when a setter is
called for a property,
it's going to call this
request update method.
That's going to
schedule an update task,
but it's only going
to schedule one
if there isn't one existing.
If there is one, we're just
going to use that same task,
and that's how we
get the batching.
When that task runs, it's
going to call the update method
on the element.
And that's where the actual work
is done to render to the DOM.
So we do this for two reasons.
One is performance,
like I mentioned.
And the other is
developer ergonomics.
So if we go back to
the template here,
we see that this
template renders
two different properties
in the same template.
And it's much easier to
reason about these templates
if we don't have to worry
about the order in which
these properties are set or
whether or not they've both
been set together or not.
So we'd like to take
all the changes that
are incoming for an element,
batch them together, and then
let you write a simple
declarative template to render
your element.
And so an interesting
implication of this
is that LitElement
rendering is always async.
You never opt into being
async, and you can't opt out
to being synchronous.
And when we explain
this to people,
sometimes, we get a question,
won't the UI partially update?
And the answer is no.
And I built a little animation
here to try to show this.
So here, we have a
tree of elements.
Let's assume these
are all lit elements,
and they're passing data
down the tree via properties.
And so that's our
component tree.
And then right here, we
have the microtask queue.
So hopefully in other talks
they've talked about this.
We have a queue of
microtasks that the browser
runs through to completion
before it will paint or handle
user input.
The yellow box here is
our current microtask.
And so if we have
some code that runs
that's going to set
a property on A,
that's going to cause its
microtask to be enqueued.
And then when A gets to run,
it might set some properties
on B and C. So their tasks
are going to be enqueued.
And B is going to set
some properties on D
and E. C is going to set
some on F and so forth.
And we get to run through this
entire queue until it's empty.
Once it's empty, then
the browser can paint.
And to show this with
a demo, I made a demo
here of a tree of elements.
And each one takes an
artificially long time
to render.
And so normally,
you might expect,
if you don't know how the
microtask queue works,
that these might
paint in individually.
And we'll see here that if we
click the render button here,
that they all snap in at once.
So even though each one
takes 50 milliseconds
and the whole thing
takes 750 milliseconds,
we don't see the
intermediate states.
And this is great if your UI
is painting fast, if it's not
taking 750 milliseconds.
But if you have a
very complex tree
and UI is rendering
slowly, then we've
just introduced jank,
which we don't want.
So this brings us to
the next technique,
which is non-blocking rendering
to keep a responsive UI.
So we just saw that we
can have async rendering,
but still block paint and input.
And we know we can
have complex UIs that
take a long time to render.
And we know we need to render
in less than 10 milliseconds
to keep our 60 frame
per second target.
And one way to look
at this is that we
have all these microtasks
here in the blue rectangles,
and they fill out
a complete task.
And this task blocks rendering.
And as long as the complete
task fits within our 10
millisecond budget, we're fine.
Right?
But as soon as the task
exceeds the budget,
we're going to introduce jank.
So our technique here
is to break this up.
So that instead of having
a whole bunch of microtasks
in one long task, we just give
a task per component to render.
And now, these will hopefully
fit in under 10 milliseconds.
And we will get smooth updates.
So the way we're going
to do this is we're
going to tap into this
asynchronous update pipeline
that LitElement has.
And we're going to customize
the schedule update task
step right here.
So that brings us to our
first experimental helper
that we're calling for
the moment LazyLitElement.
The way this works is that
under the hood in LitElement,
there's a method
called scheduleUpdate.
And by default, this thing
just waits for a microtask,
and then it calls
validate, which
does the work of rendering.
And so what we do
in LazyLitElement
is we override this.
And instead of waiting
for a microtask,
we wait for a promise that's
resolved on setTimeout timing.
It's a very simple
thing to do, but it
lets the browser paint and
handle input before we render.
So now if we go back
to this demo here,
we can turn on lazy rendering.
And now, everything is going
to render on setTimeout timing.
And you can see that we paint
the intermediate steps here
as we go.
And so we've reduced
jank by showing
some intermediate state.
And so a lot of
frameworks have been
working on asynchronous
rendering over the years,
and especially React recently.
And they have created a
demo that I quite like,
which is a Sierpinski
triangle demo.
And the way this works is
that you have a large tree
of components here.
And each one of
these has also been
written to take an artificially
long amount of time,
and they all have a label.
And this label represents
data flowing down the tree.
So to update the label
on all the components,
it's going to take a
little bit of time.
And while we're
updating the label,
we're going to animate
the size of the tree.
And we want this
animation to be smooth,
and it's driven from JavaScript.
So if we take a long
time to update the tree,
we're going to get jank.
This is a nice demo
because it highlights
some subtleties that you
need to take care of when
doing asynchronous rendering.
So we have an expensive
subtree to render.
We want this continuous script
driven animation to be smooth.
And then on top of that, we
have these high priority inputs
that we also want to handle.
So I implemented this
here with regular Element
that uses the microtask queue.
And you can see that as the
triangle updates in size,
we get some jank in
the middle there.
And we want to avoid that.
So it's very simple to
re implement this just
by changing the base
class to LazyLitElement.
And now you can see that we
get a smooth animation even
as we update the labels here.
But next, I mentioned we want
to have these high priority
inputs.
So this brings us to the
idea of urgent updates.
If you defer rendering,
it's possible
that you have
situations where you
want to render sooner than
you've scheduled yourself
to be rendered.
And so with these urgent
updates what we've done
is we've created
in LazyLitElement,
we don't just override the
schedule update method.
We add a new method on here
called requestUrgentUpdate.
And that's going to be
called and make your element
render sooner.
It's a very simple
implementation.
I wanted to show it, because
it's a little bit interesting.
So instead of waiting for a
promise that resolves with
setTimeout-- we still do
that, but we also store--
Let me go back here.
Maybe not.
OK.
Well, we restore
the resolve function
on the instance of the element
when we schedule an update.
And then we can go back and we
can call that resolve function,
which will cause our
promise to resolve earlier
than it was scheduled to.
So in essence, we're
jumping from the task queue
over to the microtask
queue, and we're going
to render as soon as possible.
OK, so this is how
you would use it.
So we have a partial
implementation
of our DotElement here.
And these are some
event callbacks
that might be called from on
mouse over and on mouse out.
And they're simply going to set
the state that our rendering is
based on, and then
they're going to call
requestUrgentUpdate to kick
us to the faster timing.
And so once we do that,
you can see that we
have our smooth animation.
Our labels update.
And we get a fast
hover over effect
here by calling
requestUrgentUpdate.
And let me talk real
quick about scheduling.
So in that demo there I
did a very simple thing.
I said instead of
using a microtask,
we're going to use a full task.
And I actually didn't
expect that to work
as well as it did
when I made the demo,
but it did work very well.
The browser ends up
doing a very good job
of executing as
many tasks as it can
before it has to paint a frame.
But it's pretty naive, and it
leaves off a lot of features
that we would like, like
different priority queues,
the ability to cancel work,
the ability to coordinate
long chains of tasks that
are all related together.
So that schedule update
method is exactly where
we would like to plug into a
native platform scheduler API,
like Shubhie and Jason
talked about earlier.
So the important point is
that with web components,
we don't have a overarching
framework that can coordinate
and schedule our
components for us,
and we might get components
from different vendors.
So being able to plug
into a global platform
vendored API for scheduling is
going to help us tremendously
here.
OK, there we go.
Let's move on to talking
about managing async state.
So far we've talked about being
asynchronous on a per component
level, so yielding
to the browser
and letting it paint
in between components.
But sometimes, we
need to manage data
that itself is asynchronous.
And lit-html rendering
is synchronous by nature.
When you give the render
function a template,
it's going to do all the work
immediately to render that.
So what do we do if we want to
render asynchronous data inside
of a synchronous template?
So we can look at how we
handle data normally here.
And if we have a string
and just a plain reference
to that string, it's
pretty easy to use.
We just use it as a template,
and we get the rendering
that we wanted.
And if we change this instead
to load off the network,
it turns out that lit-html
handles promises already.
And so what we'll get
is a blank screen here.
And then when the
promise resolves,
we'll render hello world.
So this is kind of nice.
We get some behavior
that we might
expect right out of the box.
But we might not want
to render a blank screen
as our initial state.
So this brings us to
the idea of directives,
and these are functions that
are a little bit special
and they're able to
customize how templates
are rendered by lit-html.
And one of the more
useful directives
that lit-html ships
with is called until.
What until does is
it takes a promise
and it will render the result of
that promise when it resolves,
but it will render a placeholder
until that promise does
resolve.
So we can use that here.
And you see that
in the template,
we call the until directive
with our promise in the loading
place holder.
And that's going to show first.
And then when it
resolves, we're going
to render our content there.
So this example is a
little bit too simple,
because it assumes that we have
this promise available already
that we want to use.
And a lot of times,
instead, we want
to run some tasks when
we need to render.
And we might have
some operation that's
dependent on some instant state.
So in this version
of the example here,
we have a file name property.
And we want to fetch some
data based on that file name.
Now we might be
tempted to call fetch
in line with the
template, so that we'll
fetch the correct file
and then render it.
And this does work, but it has
a problem where every time you
render this template,
we're going to call fetch.
And we might be
rendering the template
because some other
properties change.
And in this case, we're
going to flood the network
with lots of fetch requests.
And we also might show
an alternating loading
and resolved state in our UI.
But it's almost the
mental model that we want.
Right?
So what we really
want to do is we
want to be able to run a task
that's dependent on some data
only when that data changes.
So that brings us to the
next experimental helper
here that we call runAsync.
And what runAsync does is it
performs an async operation,
but only when the data
it depends on changes.
And it's actually a
directive factory.
So the way it works
is that you give it
in async function
that takes some data
and produces a result,
and it returns to you
a directive that you can use
inside your lit-html template.
So if we want to reproduce the
previous example here using
this fetch, we can just create
a fetch content directive
by passing runAsync a function
that takes a file name
and calls fetch for us.
And when we go to
use it, we can just
use it inside of our template.
And we pass it the
file name here,
and then we pass it
another template that's
going to render when
we have successfully
resolved that promise.
So this lets us get part of
the way to our goal here.
We can render some
asynchronous data,
but it turns out that
asynchronous data
can be in a number
of different states.
For any async operation, you
can be in initial state, which
means you haven't started it.
You can be pending.
It can have successfully
completed or ended in failure.
And so it really helps
us if we model and think
about all of these
states explicitly,
so that we make
sure we handle them.
We can see that our
directive actually
takes templates for all
of the different states
that our UI can be in.
So we have a success
template, a pending template,
a initial state template,
and an error template.
And to make this a little
bit more realistic,
I made a demo that searches
the NPM package repository.
And this is a
basic search as you
type live search result demo.
And it has a simple UI.
We just have a search box and
a results panel down here.
OK, so we're going to build this
demo in two parts, hopefully.
So first, we're going to define
a searchPackages directive
using runAsync.
And so here's our
initial starting point
for this directive.
Our async task
function here is going
to generate a URL for
the NPM search service
here and then get the
results by fetching it.
And then here, we're going
to handle the response
and just do a little
bit of due diligence
and make sure that we have a
200 response and return that.
Otherwise, we throw the
message we got back.
And I wanted to make
this task able to trigger
that initial state template.
And the way we do
that in runAsync, we
throw an initial state error.
So here, I'm just going
to check to make sure we
have a query we can execute.
And if not, I'm going
to throw this error.
And runAsync is going to render
the initial state template.
And then it turns out
that the NPM registry
is a little difficult to
get it to trigger an error.
Usually, it just
returns empty results.
So to be able to show
the error state template,
I just do some prevalidation
here of the query
and make sure we don't start
with the dot or underscore.
And then finally, to make this
even more realistic, if you're
doing a search as
you type UI, you're
going to have a lot of requests
that you initiate where you
don't care about the
results, because you've
typed in a different query by
the time the result gets back.
So the fetch API is
able to take something
called an abort signal, so
that we can cancel the request.
And so runAsync will generate
an abort signal for you.
And then you get it in
this options argument here
and you can forward this
on to the fetch API.
And so this is our entire
searchPackages directive
here built with runAsync.
And so next, we
just need to use it.
Here's a little
snippet of the demo UI.
We have an input
element here, which
just simply displays the query
and updates the query on input.
And then down here, we use
the searchPackages directive.
And so we use it by
passing it to query.
And then, we give it
a success template.
Here, we just iterate
over the results
and display little cards.
And then we give it
the pending, initial,
and error state templates here.
And so when we go
to use this demo,
we see that we have the initial
state template rendering.
Sorry that's small.
It says enter a search term.
When we type, it
turns into loading.
And then we get
our results back.
And if we were to go back
and enter a query that
starts with an
invalid character,
you're going to see the error
template there, and that even
updates as you type.
And if you realize
your mistake, and go in
and type in a new
term, you're going
to get all the
intermediate async state
templates as you type.
So that's the demo,
and you really
did see most of the
implementation there.
So it was quite easy to
do with that directive.
And the key takeaway
here is that we
want to explicitly model our
asynchronous operation state.
If we do that, we're more sure
to take care of all the states
that we can be in.
And if we build a
UI for each state,
then we're going
to accurately let
our users know what is going
on with the application.
And they're going to have
a better user experience.
OK, so finally, once
we have an application
and a UI built up of all
these asynchronous components,
we might need to
coordinate them.
Right?
So if you have a lot of
async children in your page,
how do we ensure a
consistent UI if you want to?
Or how do we avoid
a sea of spinners?
And so to demonstrate the
sea of spinners problem,
I added to the demo
a little feature here
where when you
search, the cards are
going to do their own
query to bring up the disk
tag of your NPM packages.
And you can see there that
we saw a lot of spinners
on the page, and this
can be a distracting UI.
So we want to give
developers an option
to not have to deal with
the sea of spinners.
And the way we're
going to handle this
is that we're going to
coordinate the components here
with events.
So what we're going
to do is we're
going to fire a
promise carrying event.
And that promise is going to
resolve when some work is done.
So an async child component
creates a promise,
fires this event, and
then resolves the promise.
And so that's going
to look like this.
A is going to be our
container up there.
And E and F are
our async children.
And they're going to
fire this pending state
event that holds a promise.
The container is going
to handle the event
and wait for that promise.
And then the children
when the work is done,
they're going to
resolve the promise.
And finally, when all the
promises have settled,
we're going to update the UI.
So that brings us to our
last experimental prototype
here called PendingContainer.
And PendingContainer takes care
of all this plumbing for you.
And it's a class mix in,
so you can apply this
to a superclass like LitElement.
And this has two features too.
We have the has pending
children property.
So this indicates
whether or not there's
async work happening below you.
And when this
property changes, it's
going to cause a
rerender of the element.
And then we have
an event listener
that listens for the
pending state event
and then triggers
the bookkeeping
so that we know if we have any
extra work pending below us.
And to use it, you can
just apply this mix
in to the superclass
here, to LitElement.
And once you do that,
you get available
the has pending
children property that
can be used in your template.
And so now we're going
to add a spinner,
and this is a top level
spinner, to the UI that's
triggered based
on whether or not
there's any pending children.
And so the runAsync
helper is going
to fire these events for us.
And this container mix in
is going to capture them.
And so what we're
going for here is
a UI where we have a spinner--
and it happened again.
There we go.
OK.
Might just be a
faulty button here.
So what we want to add here is
a spinner at the top of the UI
that's going to be going
whenever there are pending
search results coming
back from the server,
or we have children
that need to update.
So now you see we get
the spinner as we type.
We don't get the
spinner on the children,
but we can see the top level
spinner is still going.
And then the results come
in and the spinner stops.
And so that's the UI
we were going for,
and it was pretty
straightforward to build
with these directives.
So when you have
an asynchronous UI,
there's a lot of
different options
that you have for
how to handle this.
You could try to block the UI
while you have pending work.
You could show the raw
incremental updates.
You could have individual
spinners on your page,
or you could try to replace
that all with top level
placeholders and spinners.
So what you want to do
depends on the UX and the UI
that you're trying to build.
But the key here is to provide
the plumbing and the framework
and the nice API so
that you can build
whatever you choose to build.
And that went two
slides forward.
All right, well,
this is my wrap up.
So we're very excited about
some additional work that's
going to be done in this area.
So Jason and Shubhie talked
about the native scheduler
API, which we're
extremely excited about.
Display locking
is a new proposal
where you're going
to be able to lock
an entire portion of
your screen so that you
can update it
incrementally and then
flip it to the new version.
And Greg talked about
virtual scroller,
which is going to let us handle
large amounts of data as well.
And then on our end, we're going
to be working on more libraries
and examples of how to
do things like lazy load
components, background
rendering, and viewport base
visibility rendering, so
that things only render when
they show up on the screen.
I have a few links here.
These will probably be easier
to get to from the video.
And then I'm going to be
over in the Ask Chrome area
doing Q&A after this, if you'd
like to ask any questions.
OK, thank you.
[MUSIC PLAYING]
