spotifyovercastrssapple-podcasts

Performance in Elm

We talk about performance tuning Elm applications.
August 16, 2021
#37

Transcript

[00:00:00]
Hello Jeroen.
[00:00:01]
Hello Dillon.
[00:00:02]
And what are we talking about today?
[00:00:03]
Today we're talking about performance, which we've never talked about before really, I
[00:00:08]
think.
[00:00:09]
Not so much.
[00:00:10]
And so you've been sort of pretty deep in some performance analysis stuff for some Elm
[00:00:20]
Review rules, right?
[00:00:22]
Yeah.
[00:00:23]
So I recently published Elm Review Performance, which has for now only one rule, which is
[00:00:29]
about detecting tail call optimizations or lack thereof.
[00:00:35]
So do you know what tail call optimization is?
[00:00:37]
I do know what tail call optimization is, but it still somewhat confuses me what the
[00:00:47]
actual performance implications are.
[00:00:49]
I understand that it's putting a stack frame for every function call that it's recursively
[00:00:55]
calling if it's not tail call optimized and you can reduce that stack frame.
[00:01:00]
But I don't understand what are the performance characteristics of like adding that stack
[00:01:06]
frame for a recursive call versus just having the memory as it goes through a while loop
[00:01:13]
or whatever.
[00:01:14]
So that's sort of hard to wrap my brain around.
[00:01:16]
Also, sometimes it's difficult to understand how do you transform something that's not
[00:01:21]
tail call optimized to something that's tail call optimized.
[00:01:24]
Those two things confuse me.
[00:01:26]
Yeah.
[00:01:27]
So maybe let's start with the basics.
[00:01:30]
First of all, what is a recursive function?
[00:01:32]
So recursive function is a function that calls itself.
[00:01:36]
So for instance, list.length is a recursive function.
[00:01:42]
I thought you were going to say a recursive function is a recursive function.
[00:01:46]
And to understand a recursive function, you have to understand what a recursive function
[00:01:49]
is.
[00:01:50]
I totally missed my joke there.
[00:01:52]
Yeah, come on.
[00:01:53]
I'm going to try to understand.
[00:01:55]
No, I usually never do the recursive jokes because when I try to do the recursive jokes,
[00:02:03]
that's when I try to do the recursive jokes and that's when I try to do the recursive.
[00:02:07]
Yeah.
[00:02:08]
You don't want to get too stuck on that.
[00:02:09]
Yeah.
[00:02:10]
And that's where tail call optimization comes in.
[00:02:12]
Actually, it doesn't.
[00:02:13]
Yeah.
[00:02:14]
I can't stop you from not having a base case, but still a good day.
[00:02:17]
So a recursive function is one that calls itself.
[00:02:20]
So list.length.
[00:02:21]
I actually don't know if it's implemented in JavaScript or not, but let's imagine it
[00:02:25]
is in Elm.
[00:02:26]
So how would you implement that?
[00:02:28]
You would do a case of the list.
[00:02:32]
So case list of, and if it's empty, then return zero.
[00:02:37]
And otherwise you return one plus list.length of the rest.
[00:02:42]
So list.length calls itself.
[00:02:45]
So you would add one for every element.
[00:02:48]
So that is recursive.
[00:02:50]
A tail call recursive function is one where it's the same thing, but it's just more optimized.
[00:02:57]
So as you said, what happens when you do a function call in JavaScript or in a browser
[00:03:05]
or an engine?
[00:03:06]
When you do a function call, it basically adds the current position and the new position
[00:03:12]
to a stack, the functions call stack.
[00:03:16]
Yes.
[00:03:17]
I'm not going to use the technical terms because I'm not aware of those.
[00:03:21]
But yeah, basically there's a stack.
[00:03:23]
So that's once you return from a function, you know where in the code the engine needs
[00:03:29]
to go back.
[00:03:31]
Right.
[00:03:32]
It's basically like a go to instruction that tells it at a low level, like when you're
[00:03:40]
done, I'm going to store the return value of calling this function in this memory location
[00:03:46]
and I'm going to, and then jump to this code.
[00:03:49]
So that's like generally what's happening there.
[00:03:52]
Yeah, absolutely.
[00:03:54]
So that is pretty cheap, but it does have a cost.
[00:03:58]
Yeah.
[00:03:59]
Right.
[00:04:00]
So if you're recursing 10 times, then you're probably not going to notice it.
[00:04:05]
Yeah.
[00:04:06]
But if you're doing list.length on a list of size 10 and it's not tail call optimized,
[00:04:11]
that's probably okay.
[00:04:12]
Yeah.
[00:04:13]
But if you're doing a thousand or 10,000 or a hundred thousand calls or lists of that
[00:04:18]
size, then it becomes noticeable.
[00:04:21]
And you also have a different problem, which is that the call stack has a limit.
[00:04:27]
Yep.
[00:04:28]
Which when you go past that, that is called a stack overflow.
[00:04:32]
Right.
[00:04:33]
And when you have a function that can give a stack overflow, that's called stack safety.
[00:04:38]
It's not stack safe because it can trigger a stack overflow if it's called with too large
[00:04:44]
of an input or whatever.
[00:04:46]
So in Elm, we say that everything is safe, but you can still trigger stack overflows.
[00:04:52]
So if you actually want to know whether a recursive function is tail call optimized,
[00:04:57]
what you can do is try to write a test where it recurses more than about 10,000 times.
[00:05:05]
And if that doesn't create a problem, if that doesn't crash, then it's sufficiently optimized.
[00:05:13]
And if it crashes, well, it isn't.
[00:05:15]
And you have a runtime error.
[00:05:18]
And you can also run your Elm review performance rule to identify these areas that might not
[00:05:27]
be stack safe, which is really neat.
[00:05:30]
So what is the Elm review rule doing to determine whether something is tail call optimized or not?
[00:05:38]
I remember you saying when you got this working, you'd been thinking about it for a long time.
[00:05:43]
Yeah, almost two years.
[00:05:45]
And then you finally realized how to do it.
[00:05:47]
And you're like, oh, it was actually really simple to implement once I realized what I needed to do.
[00:05:52]
So before I explain that, I think it's nice to explain how a tail call optimized function works.
[00:05:58]
Right.
[00:05:59]
And compared to recursive function.
[00:06:01]
Maybe we should point out that this is an optimization that the Elm compiler just has built in.
[00:06:07]
Yes.
[00:06:08]
That it's turning our code into JavaScript code so it can go ahead and say, oh, you know what?
[00:06:14]
I could rewrite this in a way that doesn't actually do recursive calls under the hood,
[00:06:19]
but I know it's going to give the same result.
[00:06:21]
So that's what we're talking about here.
[00:06:22]
Yeah.
[00:06:23]
So recursive function, a plain one, when it calls itself, it adds to the stack call.
[00:06:30]
An optimized one doesn't have a stack call or less so.
[00:06:35]
More about that later.
[00:06:37]
Right.
[00:06:38]
But it's literally a while loop in the compiled Elm code.
[00:06:41]
Yes.
[00:06:42]
So instead of calling itself, what it does, it creates a while loop with the current element to be,
[00:06:51]
the current arguments, defined outside of the while loop.
[00:06:54]
And the while loop just updates those arguments to be whatever would be the next arguments,
[00:07:00]
the arguments of the next function call.
[00:07:02]
Right.
[00:07:03]
So list.length would be a while loop with a count defined outside the while loop.
[00:07:10]
And in the while loop, it would increment.
[00:07:14]
And the list to be analyzed would be reduced at every step.
[00:07:19]
Is that clear enough?
[00:07:20]
Yes.
[00:07:21]
Okay.
[00:07:22]
So is that able to allocate less memory?
[00:07:26]
Because is it taking the memory scope with the recursive calls if it's not tail call optimized?
[00:07:34]
So the way I understand it, for every call stack, you need to allocate a new variable.
[00:07:39]
Right.
[00:07:40]
Or new variables.
[00:07:41]
Yes.
[00:07:42]
Right.
[00:07:43]
So if you don't have to allocate any new ones, then yeah, you have a lot less to allocate.
[00:07:49]
Right.
[00:07:50]
So that also takes time.
[00:07:52]
So this is time that is saved.
[00:07:55]
And you also don't have to do the call stack and the changing the position.
[00:08:00]
Popping off the call stack and everything.
[00:08:02]
Yeah.
[00:08:03]
Yeah.
[00:08:04]
Popping on, popping off.
[00:08:05]
Yeah.
[00:08:06]
Right.
[00:08:07]
Right.
[00:08:08]
So very tiny things, but when you do 10,000 of them, it matters.
[00:08:12]
Right.
[00:08:13]
So, and we should, you know, stepping back a little to like the why here now, and I think
[00:08:19]
this is, I think you and I both feel the same way here that we're not necessarily experts
[00:08:26]
by any means on performance.
[00:08:29]
But one thing that we can say with confidence is that you should measure performance to
[00:08:34]
identify bottlenecks and not assume that number one, don't assume that something needs to
[00:08:41]
be optimized unless you know it needs to be optimized because you actually benchmarked
[00:08:46]
it.
[00:08:47]
Number two, don't assume that a particular change is in fact going to yield better performance
[00:08:52]
because it's going to do surprising things.
[00:08:55]
Also Elm compiles into JavaScript.
[00:08:59]
That's one layer of indirection.
[00:09:00]
So Elm compiles to JavaScript.
[00:09:03]
You don't know exactly what JavaScript it compiles to.
[00:09:06]
You have some sort of vague sense of if you're calling a function, it's probably going to
[00:09:11]
compile to something that's calling a function in Elm, but you don't know exactly what it's
[00:09:16]
compiling to and what the performance characteristics of different code will be.
[00:09:21]
You can look at it though, because the source code is pretty readable compared to what you
[00:09:25]
might expect.
[00:09:26]
Yes, you can look at it, but also then that JavaScript code is being run by V8 or whatever
[00:09:33]
JavaScript engine and these JavaScript engines do all sorts of really nuanced optimizations
[00:09:41]
with the just in time compilation.
[00:09:44]
I mean things like tail call optimizations, I don't know if any JavaScript engines have
[00:09:49]
that built in now.
[00:09:51]
I know that Evan added tail call optimizations to Elm because at the time the JavaScript
[00:09:58]
engines didn't have that.
[00:09:59]
Yeah.
[00:10:00]
I think it's there in a few.
[00:10:02]
Maybe only one, but not all of them.
[00:10:05]
Okay, yeah.
[00:10:06]
But the point being that don't try to predict what's going to perform well because all sorts
[00:10:11]
of unexpected things are going to perform very well or very poorly counter to your intuition.
[00:10:17]
So just assume that you don't know whether something is going to perform well or not
[00:10:22]
unless you benchmark it and assume that an optimization you try to make without measuring
[00:10:28]
is not necessarily going to improve things.
[00:10:31]
Yeah, tail call optimization is usually still a good one, but if you need to change how
[00:10:36]
the function works to make it work, then yeah, benchmark it.
[00:10:41]
We'll talk about benchmarking later.
[00:10:44]
Yes.
[00:10:46]
So to wrap up tail call optimization to pop the stack again and take our call frame back
[00:10:52]
up there.
[00:10:53]
So in order to optimize, in order to turn something into a tail call optimized invocation,
[00:11:01]
what do you do to do that?
[00:11:03]
So as you said before, the compiler already does it for you.
[00:11:07]
It optimizes function for you, but only if the function has a certain shape.
[00:11:12]
Basically it wants the return value to be in specific positions.
[00:11:17]
So the return value can be in several positions.
[00:11:20]
And I say return value, I mean the recursive call.
[00:11:23]
So it needs to be at the end of a branch.
[00:11:26]
So if you can do a recursive function inside an if else, for instance, you could do it
[00:11:31]
in both branches, the if or the else, but you cannot do it in the condition.
[00:11:36]
You can do it in the branches of a case expression.
[00:11:40]
You can do it in the in of a let expression.
[00:11:43]
So let blah, blah, blah in, in there you can do the recursive call.
[00:11:46]
And that's pretty much it actually.
[00:11:48]
You can compose them together so you can do a recursive call in a if inside of a case
[00:11:54]
branch and that works.
[00:11:56]
And that's it.
[00:11:57]
Like basically it's the recursive call needs to be the last operation that happens.
[00:12:02]
Right.
[00:12:03]
So if you were to do a recursive call plus recursive call, suddenly you're not in the
[00:12:12]
simple case, you know, possibilities that you laid out of case expressions, if expressions,
[00:12:18]
let bindings.
[00:12:20]
It's now a plus expression.
[00:12:21]
A plus expression.
[00:12:22]
Yeah.
[00:12:23]
Yeah.
[00:12:24]
So one thing that is, for instance, a bit odd is like when you call the function recursively
[00:12:30]
using a pipe, like you do one pipe list dot length.
[00:12:35]
Right.
[00:12:36]
Oh yeah.
[00:12:37]
Lists pipe, this is a length.
[00:12:40]
Unfortunately that is not considered function call by the Elm compiler.
[00:12:44]
So it doesn't look like what it expects, therefore it doesn't optimize it.
[00:12:48]
So it really is the if the case and the let's.
[00:12:52]
Right.
[00:12:53]
Which just happens to be with the, with the architecture of the Elm compiler and the,
[00:12:58]
the different passes that it does in compilation.
[00:13:01]
It doesn't see that as it could, but it doesn't.
[00:13:05]
So that's just a thing to know if you're trying to do a tail call optimization.
[00:13:09]
And again, like you should benchmark before you assume that you need that, especially
[00:13:16]
if it's going to make the code more complex.
[00:13:20]
You shouldn't just assume that everything has to be tail call optimized.
[00:13:24]
And but it's a good idea.
[00:13:27]
I mean, it's one area that you could get a runtime exception.
[00:13:31]
Right.
[00:13:32]
So that's a reason to prefer tail call optimizations in itself.
[00:13:36]
Yeah.
[00:13:37]
That's like the biggest reason to do it because the performance gets better, but not that
[00:13:43]
much better.
[00:13:44]
Like a few percent.
[00:13:45]
Right.
[00:13:46]
And if you're, if you're designing a package that is doing, you know, using large data
[00:13:51]
sets in a lot of use cases, then you want to do all these micro optimizations to squeeze
[00:13:57]
a little bit of performance.
[00:13:59]
Of course with, with benchmarking to guide where those performance opportunities are,
[00:14:04]
but for application code, you very much want to avoid runtime exceptions.
[00:14:08]
Yeah.
[00:14:09]
So the Elm compiler does this optimization.
[00:14:12]
I know that other compilers, other languages do other kinds of optimizations, which are
[00:14:16]
more powerful, more useful.
[00:14:19]
I'm not exactly sure what they are, but I've been told that there are.
[00:14:22]
Yeah.
[00:14:23]
And that Elm isn't actually, doesn't actually do tail call optimizations, like optimized
[00:14:28]
functions.
[00:14:29]
So like sale, self call, sale recursion, which is only a subsets.
[00:14:35]
But yeah.
[00:14:36]
Yeah.
[00:14:37]
I don't know much more than that.
[00:14:38]
So what does my Elm review rule does do?
[00:14:42]
So the only thing it does is check whether you have recursive calls in other places than
[00:14:48]
the ones that I mentioned in if, in case.
[00:14:50]
Right.
[00:14:51]
And stuff like that.
[00:14:52]
And that's all it does.
[00:14:53]
Yeah.
[00:14:54]
And yeah, I've been thinking about this rule for a year and a half, two years, maybe like
[00:14:58]
when I started working on Elm review basically.
[00:15:02]
And I was thinking, is it is actually a bit complicated, but then when I understood that
[00:15:09]
it will just work this way, it was like, yeah, this is really simple.
[00:15:13]
This is very easy Elm review rule to write.
[00:15:17]
I feel like that's a very common process in like, I mean, whether it's API design or any
[00:15:24]
sort of like engineering, like that you think really hard in order to find the simple solution,
[00:15:29]
which then when you tell anyone about it, they're like, oh, that seems pretty simple.
[00:15:33]
You're like, yeah, but it was so hard to figure that out.
[00:15:36]
But I explained it to you in the simple way.
[00:15:40]
It took me years to get to that understanding.
[00:15:44]
It's like the Mark Twain, like forgive me for the long letter, but I didn't have time
[00:15:49]
to write you a short one.
[00:15:51]
Yeah.
[00:15:52]
Okay.
[00:15:53]
So is there anything else people should know about tail call optimizations in Elm?
[00:15:59]
So you asked before, how do you optimize recursive function?
[00:16:04]
So in some cases it's like very simple.
[00:16:08]
Sometimes it's removing a pipe.
[00:16:09]
I've made a pull request to Elm community list.extra, yeah, list.extra, where they had
[00:16:20]
a function that was not tail call recursive because they use a pipe, like a lift pipe.
[00:16:28]
Just remove that pipe and add parens and that was it.
[00:16:34]
And that was like a 7% increase in performance.
[00:16:38]
So yeah.
[00:16:39]
That's cool.
[00:16:40]
Okay.
[00:16:41]
Well, that's a good concrete number.
[00:16:42]
Nice.
[00:16:43]
Oh, another thing that I realized after I wrote my blog post and I edited it when I
[00:16:48]
made the announcement was that you have recursive functions, you have non recursive functions
[00:16:53]
and you have partially recursive functions.
[00:16:56]
So it's not actually a thing of the function is recursive, is that the call is recursive.
[00:17:06]
So if you do a recursive call inside one of those allowed places, that gets optimized.
[00:17:12]
If you do another one where it's not allowed, well that one won't get optimized, but it
[00:17:16]
doesn't deoptimize the other one.
[00:17:18]
You will have a while loop in one place and a recursive call, a plain one in a place where
[00:17:25]
it was not allowed.
[00:17:26]
So yeah, I didn't know, but you can have partially recursive functions.
[00:17:32]
Partially tail call optimized.
[00:17:33]
Yes.
[00:17:34]
Right.
[00:17:35]
Yeah.
[00:17:36]
That's really cool.
[00:17:37]
I didn't realize that until I read your blog post either, which we will link to in the
[00:17:41]
show notes.
[00:17:42]
Also, Evan made a great post on how to explain.
[00:17:48]
Also Evan wrote a very good article to explain how it works.
[00:17:52]
Not exactly when it gets applied, which I do better in my article, but it does explain
[00:17:57]
the reasoning for it.
[00:18:00]
Yes.
[00:18:01]
And how to transform something from a non tail call optimized form into a form.
[00:18:09]
Basically you need to sometimes make state explicit in a way that it can be passed down
[00:18:16]
recursively.
[00:18:17]
Yeah.
[00:18:18]
So for instance, how would you optimize list.length?
[00:18:20]
Right.
[00:18:21]
So I think the problem is that if you do, you need to keep track of the running length
[00:18:31]
that you have so far.
[00:18:33]
Because previously we said list.length is basically...
[00:18:36]
One plus.
[00:18:37]
Plus.
[00:18:38]
Plus operation.
[00:18:39]
Plus operation.
[00:18:40]
Right.
[00:18:41]
Yes.
[00:18:42]
So you would need to add an extra argument to get the length so far.
[00:18:49]
And then you would need a list.length.help function that would start with the length
[00:18:55]
so far as zero.
[00:18:56]
Yeah.
[00:18:57]
Usually you split them up into a function, a public function and a helper function.
[00:19:03]
Right.
[00:19:04]
So the public one bootstraps it with some default value, some initial starting value.
[00:19:10]
And then that's sort of like what the stack frame would have been carrying around, that
[00:19:18]
state of the running length.
[00:19:20]
But you're making that an explicit part of the quote unquote state of this recursive
[00:19:26]
call.
[00:19:27]
So you make that explicit and now you can do list...
[00:19:30]
What would it be then?
[00:19:31]
The length of the rest of the list?
[00:19:35]
And is there a plus one in the function invocation?
[00:19:38]
Or where does that plus one go now?
[00:19:40]
Yeah.
[00:19:41]
So I'm going to say it out loud in code.
[00:19:45]
So you would have length which takes a list and that equals list.helper the list zero.
[00:19:55]
And list.helper would...
[00:19:56]
So it takes the list and the results so far.
[00:20:01]
Yes.
[00:20:02]
So you do a case off of that list.
[00:20:04]
If it's empty, you return the results so far.
[00:20:07]
So the accumulated result.
[00:20:10]
And if it's not empty, then you call the function itself with the rest, but the results so far,
[00:20:17]
you add plus one to that.
[00:20:18]
So you do list.helper rest of list and then in parens results so far plus one.
[00:20:24]
Right.
[00:20:25]
So you can do addition in the arguments.
[00:20:27]
Yes.
[00:20:28]
And that's not an issue.
[00:20:29]
But if you turn the recursive function call expression into a plus expression, that deoptimizes
[00:20:37]
that invocation.
[00:20:39]
Yeah.
[00:20:40]
So this is a fairly simple example.
[00:20:44]
Sometimes it's a lot more complex.
[00:20:45]
Like if you need to do recursive...
[00:20:49]
Like when you need to do recursive applications on a tree, like you need to call the recursive
[00:20:57]
function for what's on the left of the tree and what's on the right of the tree.
[00:21:01]
And then you need to combine them together.
[00:21:04]
And that is by definition an operation you do on the result.
[00:21:08]
So that doesn't work.
[00:21:09]
So what you can do is one technique at least is to emulate a stack.
[00:21:14]
So the stack of things that you still need to compute, you make that an argument.
[00:21:19]
Just like the results so far we had before.
[00:21:21]
You make the stack an argument.
[00:21:23]
And maybe also the results so far as a second argument.
[00:21:26]
It feels similar to doing a list fold a little bit.
[00:21:29]
That you have the accumulator and that has all the state that you need.
[00:21:34]
And list.fold is telecooptimized in that way.
[00:21:39]
The difference is that list.fold, you already know beforehand all the elements of the list
[00:21:45]
that you're looping over.
[00:21:48]
Which in recursive function, it might not be the case.
[00:21:52]
Yes.
[00:21:53]
Cool.
[00:21:54]
And I think that's a good thing to know when you're writing Elm code.
[00:22:01]
I gave my rant about how you should benchmark before you assume something is a problem.
[00:22:08]
And to me, the way I think about this stuff is you want to be aware of as many of these
[00:22:14]
sorts of things as possible.
[00:22:16]
So that when you need to improve performance, when you know that there's a bottleneck somewhere,
[00:22:25]
you know what to look for.
[00:22:26]
For opportunities for what to optimize.
[00:22:29]
So you can look at something and you realize you have this burned into your brain that
[00:22:35]
if you do recursive call plus recursive call, oh, that's not telecooptimized.
[00:22:41]
Just having these patterns and being aware of them is very helpful, I think.
[00:22:46]
Julia still writes it, like you just said.
[00:22:50]
Recursive call plus recursive call.
[00:22:53]
And that's fine.
[00:22:55]
In most cases.
[00:22:56]
And then when you really want to optimize, that's when you transform it in a way that
[00:23:00]
is better for performance.
[00:23:02]
But you need to benchmark it.
[00:23:04]
I'm actually very bad at doing that.
[00:23:08]
It's not easy.
[00:23:09]
Yeah.
[00:23:10]
For this one, I would be like, I'm not sure it's going to help performance.
[00:23:14]
But I do care about stack safety.
[00:23:15]
So that's something already.
[00:23:17]
Right.
[00:23:18]
So we should, we should talk about ways to benchmark in Elm.
[00:23:23]
So one of one of my favorite ways to benchmark is just to run Lighthouse.
[00:23:28]
Well that's true too.
[00:23:30]
I mean, to a certain extent, like, so I think we shouldn't assume that performance is a
[00:23:36]
problem unless we benchmark and see it is.
[00:23:39]
But we shouldn't assume that performance is good without benchmarking it either.
[00:23:44]
For example, most of us are going to be using high powered machines that are more powerful
[00:23:50]
than most users are used to.
[00:23:52]
Maybe internet connections that are more powerful than most users are using.
[00:23:56]
You know, a lot of users are going to be accessing our sites on mobile devices, which have far
[00:24:00]
slower internet connections.
[00:24:02]
They don't have 32 gigabytes of RAM.
[00:24:05]
They don't have 32 gigabytes of RAM and 32 cores on their machine.
[00:24:09]
So wait, you have 32 cores?
[00:24:11]
No, I don't have 32 cores.
[00:24:13]
I wish.
[00:24:15]
But they, it takes a lot more to even just like run JavaScript at all and you know, do
[00:24:23]
the run the just in time compilation and all of these fancy things that we're running.
[00:24:28]
So running Lighthouse can give you a little bit of a clue because it will throttle, you
[00:24:33]
know, it'll simulate throttling the network to simulate using a mobile device and simulate,
[00:24:39]
I think it simulates like degrading performance for the mobile Lighthouse.
[00:24:44]
Yeah, I think as well the CPU, I think so.
[00:24:46]
Yeah.
[00:24:47]
Which I have no clue how they do that.
[00:24:49]
Yeah, I know.
[00:24:51]
Just put a sleep between each instruction.
[00:24:55]
You know, I mean, so much effort has gone into these tools, you know, both for like
[00:25:01]
benchmarking and discovering performance issues and for avoiding them with these like very
[00:25:08]
sophisticated optimizations in like these JavaScript engines.
[00:25:12]
But it's really worth using Lighthouse and you know, the Chrome performance tab to analyze
[00:25:20]
these things.
[00:25:21]
So that's really helpful.
[00:25:24]
Understand like pull up the performance tab.
[00:25:27]
You can hit the record button and just run some JavaScript code.
[00:25:32]
If there's something that you suspect might be slow, then you can hit the record button,
[00:25:38]
do that slow operation and then hit the stop button.
[00:25:41]
And then you can see this like bottom up view and you're probably going to find a few readable
[00:25:46]
Elm function names that will point you to some of your bottlenecks in performance.
[00:25:51]
Yeah.
[00:25:52]
So do you have any good resources on how to use that performance tab?
[00:25:56]
Because it is a very nice tool.
[00:26:00]
It has a lot of options, but I'm not sure I would know exactly what everything means.
[00:26:06]
So do you have any good resources on that?
[00:26:08]
Yes.
[00:26:09]
So Jue Lu, is it Jue Lu?
[00:26:12]
Is that how it's pronounced?
[00:26:14]
Arkham.
[00:26:15]
Arkham.
[00:26:16]
We all know Jue.
[00:26:17]
Jue's great.
[00:26:18]
Check out, Jue wrote a blog post called performant Elm.
[00:26:22]
It's a two part series and it basically walks you through this.
[00:26:25]
And that was actually how I discovered this technique and it like steps you through exactly
[00:26:31]
how to do it.
[00:26:32]
And it's really helpful.
[00:26:33]
So we'll leave a link in the show notes and definitely check that out.
[00:26:37]
It's a lot easier than you would imagine.
[00:26:39]
So it's a great idea to run that and try to look for bottlenecks.
[00:26:44]
Also when you run a Lighthouse audit on your site, it now identifies if you have like long
[00:26:52]
running tasks.
[00:26:54]
So now if you want a 60 frame per second animation, so when we're writing Elm code, we're writing
[00:27:04]
something that compiles to JavaScript, which runs in a browser.
[00:27:08]
And so I think it's important to have some general understanding of performance in JavaScript
[00:27:14]
and performance in browsers.
[00:27:16]
And so one really important thing to understand, if you understand one thing about JavaScript
[00:27:23]
buttons, it should be that it's single threaded, right?
[00:27:26]
There's one thread of execution.
[00:27:29]
And if you block it, everything grinds to a halt, including scrolling and input events
[00:27:34]
and you're blocking user interaction, which is very frustrating.
[00:27:39]
Which means that you cannot click on buttons.
[00:27:41]
You cannot type anything.
[00:27:43]
Exactly.
[00:27:44]
All of these, even like built in browser animations of clicking buttons and things will just freeze
[00:27:49]
up and lock up.
[00:27:50]
And so there's like the first input delay Lighthouse metric, which helps identify this
[00:27:57]
kind of issue.
[00:27:58]
And this is starting to become the way that Google ranks sites now.
[00:28:03]
This is becoming a core metric that will actually bump you down in the search results if you
[00:28:10]
have issues with these metrics.
[00:28:12]
So it's important for SEO as well.
[00:28:16]
So if you understand one thing about JavaScript performance, it should be that there's a single
[00:28:20]
thread of execution and don't block that thread.
[00:28:23]
If you're trying to get 60 frame per second animations, that means that you have about
[00:28:29]
16 milliseconds to perform a blocking operation.
[00:28:35]
So now if you're performing an HTTP request, that's non blocking.
[00:28:40]
That's another thing about JavaScript that there are these non blocking IO operations
[00:28:45]
like performing HTTP requests.
[00:28:48]
And if your HTTP request takes five seconds, that's okay because you're not blocking the
[00:28:53]
main thread.
[00:28:54]
You're queuing that work and it's going to come back and run your single threaded JavaScript
[00:29:00]
when it's done with HTTP request.
[00:29:04]
But for the actual processing of things, that's single threaded and you've got 16 seconds
[00:29:11]
if you want 60 frames per second.
[00:29:14]
16 milliseconds.
[00:29:15]
Sorry, 16 milliseconds.
[00:29:17]
Don't take 16 seconds to do anything.
[00:29:19]
That would be really bad.
[00:29:21]
And you've got about 50 milliseconds just in general if you want to not be blocking
[00:29:28]
user interactions and having a clunky experience there.
[00:29:32]
Yeah.
[00:29:33]
But it's a bit hard to know when you reach that budget.
[00:29:36]
So basically, how to do as few work as possible.
[00:29:40]
Yeah.
[00:29:41]
Yeah.
[00:29:42]
Try to be as minimal as possible and benchmark, right?
[00:29:47]
Run Lighthouse.
[00:29:48]
If you run Lighthouse, it will tell you if you have long running blocking tasks that
[00:29:52]
take 50 milliseconds or more.
[00:29:54]
And it'll actually point you to where that happens.
[00:29:57]
So that's a great technique.
[00:30:00]
And so another thing, a lot of these performance improvements I find come down to most of the
[00:30:07]
time it's not these micro optimizations.
[00:30:10]
More often it's architectural or algorithmic improvements.
[00:30:14]
If you're doing unnecessary work, try to do less work.
[00:30:17]
If you're holding lots of stuff in memory, try to hold less stuff in memory.
[00:30:21]
If you're constantly transforming things from different data structures, if you're turning
[00:30:26]
something from an Elm list to an array to a dict and then pulling and if you're doing
[00:30:34]
indexed access into large lists in Elm or things like that, those are things to look
[00:30:38]
for.
[00:30:39]
I mean, those are usually fine when the collection size is pretty small.
[00:30:47]
You can do loads of those, no issue.
[00:30:50]
But if it's on a list of a thousand elements, you will start noticing it.
[00:30:56]
And especially if you want to reach that 60 FPS magical number, you should avoid doing
[00:31:02]
this in the view function too much or in every update or things that are recurring.
[00:31:08]
That's a great point.
[00:31:11]
Elm's virtual DOM will avoid any unnecessary work as much as it can.
[00:31:17]
If you have not received a new message, you will not update the model.
[00:31:22]
And if you don't update the model, you don't call the view again.
[00:31:27]
If between two frames no message happens, well, you're good.
[00:31:31]
You have nothing to re render.
[00:31:33]
You have no computation to do.
[00:31:34]
But if you have timed out every millisecond.
[00:31:37]
Then you're calling update every millisecond.
[00:31:42]
And you have 16 messages to handle in one frame.
[00:31:47]
So everything needs to be quite fast.
[00:31:50]
I do think that the virtual DOM or Elm slash browser is calling the view only once per
[00:31:57]
animation frame.
[00:32:00]
So the view can be 16 times as slow as the updates.
[00:32:06]
But still, you don't want to do too many messages if you can avoid it.
[00:32:14]
Right.
[00:32:15]
And there's this push and pull of you want to optimize performance and you want your
[00:32:21]
code to be nice to work with.
[00:32:23]
And those two things are sometimes at odds with each other, which is why you really need
[00:32:28]
to benchmark.
[00:32:30]
So like, for example, like one of the for me, one of the most important design principles
[00:32:36]
in an Elm application is to derive state, not store state.
[00:32:41]
If you have state trickling through and being derived, then you don't have places where
[00:32:47]
things can get stale and out of date and bugs that can come from that.
[00:32:52]
Yeah, you want to source state but not derive state.
[00:32:55]
Well, you want to derive state from the things in the model is what I, you know, we're saying
[00:33:02]
the same thing with different words here.
[00:33:04]
But yeah, so like, basically don't have information that can be derived from your state.
[00:33:10]
Don't duplicate that in multiple places, because that places for bugs to happen and for things
[00:33:14]
to go stale.
[00:33:15]
And it just makes Elm better to work with and makes your code less bug prone and easier
[00:33:19]
to maintain.
[00:33:20]
So like, if that isn't giving you performance problems, you don't want to like memoize things
[00:33:26]
to avoid computations.
[00:33:29]
That should be your last resort.
[00:33:30]
If you in some cases, you may need to do that, but you want to avoid it if you can.
[00:33:35]
And so use this is one place that HTML dot lazy can really help out right is that could
[00:33:41]
take care of memoizing to a certain extent for you.
[00:33:45]
Do you want to introduce what HTML dot lazy does?
[00:33:47]
Yeah, sure.
[00:33:48]
So as I previously Elm will not call the view function if the model didn't change.
[00:33:54]
So it does that at the root of the application.
[00:33:57]
But you can also kind of re implement that logic just by sprinkling your view code with
[00:34:03]
HTML lazy.
[00:34:04]
So basically what it does is when you there's HTML dot lazy dot lazy, which which is a function
[00:34:10]
which takes another function of view function.
[00:34:12]
So something that returns HTML and takes one arguments to if you use lazy to three, if
[00:34:18]
you take use lazy three, etc.
[00:34:22]
And if the arguments to that function did not change, then it will skip the work.
[00:34:27]
It will skip computing the function that you passed to lazy.
[00:34:32]
So that is very nice because if you have something that will rarely change like a header footer,
[00:34:39]
a part of your another part of the page, which is pretty expensive to compute, but it rarely
[00:34:45]
changes, you just sprinkle lazy in there and you will avoid a lot of unnecessary work.
[00:34:51]
Yeah, because next time, I'm renders the whole page, it won't compute that part of the page.
[00:34:56]
Right?
[00:34:57]
Yeah, you're basically proving to Elm through through the signature, it's almost like you're
[00:35:03]
taking the arguments that go through to some like view helper function.
[00:35:07]
But instead of directly calling your view helper function with with the arguments it
[00:35:11]
depends on your, you're giving these lazy helpers that view helper function, which takes
[00:35:19]
an argument.
[00:35:20]
And then you're passing those actual arguments to lazy.
[00:35:24]
And it can and it can say, Oh, if, if this argument hasn't changed, I'm not going to
[00:35:29]
re invoke this function because you've basically proven to Elm that those are the only things
[00:35:34]
that can cause that function to render a different result since they're all pure functions.
[00:35:39]
Yeah.
[00:35:40]
So that is very powerful.
[00:35:41]
And that is very easy to add.
[00:35:43]
Yeah.
[00:35:44]
The only problem is that is also very easy to mess up in any way that is very non Elm
[00:35:51]
ish.
[00:35:52]
Yeah.
[00:35:53]
So what actually needs to work for lazy to work well is that every argument needs to
[00:35:58]
be needs to have the same reference as last call.
[00:36:02]
Right.
[00:36:03]
So what it, what Elm under the hood does is for every argument that you pass to the function,
[00:36:10]
including the function itself, it does equal equal.
[00:36:13]
So is it the same function as before using equal equal or no triple equals triple equals.
[00:36:19]
So, so if it's the same, then it looks at the next argument.
[00:36:23]
If it's that's the same, etc, etc.
[00:36:26]
And if one of them is not the same, then it will recompute the function.
[00:36:30]
I see.
[00:36:31]
So for primitives like strings and integers and bullions and static references like custom
[00:36:38]
types, custom type constructors that have no arguments that will always succeed if it's
[00:36:44]
the same value, like you pass in five and next time you call it and you pass in five,
[00:36:49]
that's fine.
[00:36:50]
But when you create more complex things like records or dictionaries, they actually need
[00:36:57]
to be the same reference.
[00:36:59]
And that's where it gets tricky.
[00:37:01]
I see.
[00:37:02]
So if you compute a value, if you compute a list, if you create one in a view function
[00:37:09]
and you pass that as an argument to the lazy function, then that is a new reference.
[00:37:16]
And that means that it will not be considered the same and therefore the function will be
[00:37:21]
reevaluated.
[00:37:22]
I see.
[00:37:23]
And that is very non Elm ish.
[00:37:26]
Yeah.
[00:37:27]
And can be very confusing.
[00:37:29]
So the worst case is that it's not actually optimized.
[00:37:35]
It's no worse than that, but it's not optimized.
[00:37:38]
And that was what you were trying to do.
[00:37:39]
But so you're saying like, if I had a list and then I ran a list dot map on that, now
[00:37:47]
the reference is different.
[00:37:48]
Yeah.
[00:37:49]
Interesting.
[00:37:50]
But if I did model dot my list, then it says it's just a reference.
[00:37:55]
So it's just passing it through.
[00:37:57]
Yeah.
[00:37:58]
Unless obviously you change that function.
[00:38:02]
Right.
[00:38:03]
Right.
[00:38:04]
Usually things that come from the model are fine.
[00:38:06]
Okay.
[00:38:07]
Yeah, that makes sense.
[00:38:09]
And that's where usually people feel like they need to store derived state in the model
[00:38:14]
is because they want to pass that in as their argument to a lazy function.
[00:38:19]
Yeah.
[00:38:20]
So they do the computation in updates instead of in the view function.
[00:38:25]
Right.
[00:38:26]
So that makes the Elm code a lot less nice, but...
[00:38:30]
Interesting.
[00:38:31]
Sometimes you need to.
[00:38:32]
I wonder if you did something in every update call that said, remove this type of thing
[00:38:41]
from this list.
[00:38:43]
So the model dot my list equals.
[00:38:47]
And if you do my list dot filter, remove something from the list every single time, then that's
[00:38:56]
changing the reference every single time, I'm assuming.
[00:38:59]
We don't normally think about these things in Elm, which is I think why you're saying
[00:39:02]
it doesn't feel very Elm because we don't usually have to think about these things.
[00:39:06]
So you would almost have to avoid updating the reference in that case where the result
[00:39:13]
is the same or compute the result and say if the number of items that it results in
[00:39:19]
is the same, then don't update the reference.
[00:39:23]
So for instance, when you have a module and it uses another module which also has update
[00:39:29]
view and all those, you usually store the model of that in a record.
[00:39:36]
So you have model which has a field subcomponent, which is a model of the subcomponent.
[00:39:45]
So every time a message for that one comes, you will update model and override subcomponent
[00:39:52]
with the new value of subcomponent, even if it didn't change.
[00:39:56]
So that means that subcomponent didn't change, the reference didn't change, but model did.
[00:40:01]
So if you put model as is as an argument to a lazy function, then that one gets recomputed.
[00:40:10]
Right.
[00:40:11]
So in general, with lazy functions, it's probably a good idea to pick off the minimal set of
[00:40:17]
data that you need to pass through to avoid busting the cache, quote, unquote.
[00:40:23]
So you can do like lazy function of subcomponent.view, and pass it model.subcomponent.
[00:40:33]
That would be very good.
[00:40:35]
Yeah, nice.
[00:40:36]
That's really good to know.
[00:40:37]
Yeah.
[00:40:38]
Do you actually know how lazy works under the hood?
[00:40:42]
I don't.
[00:40:43]
Do you?
[00:40:44]
I looked it up because I was curious.
[00:40:46]
I'm guessing it's like in the virtual DOM code.
[00:40:50]
Oh man, you're better than me.
[00:40:53]
Yeah.
[00:40:54]
It is in the virtual DOM code, but where are the values stored?
[00:40:58]
The virtual DOM facts?
[00:41:01]
I don't know.
[00:41:04]
So I always thought like Elm has a global store of lazy functions and their last arguments,
[00:41:15]
which doesn't make sense because sometimes you call the same function with different
[00:41:19]
arguments in the same view, and those would then be very hard to keep track of.
[00:41:28]
And that's absolutely not how it works.
[00:41:30]
It's not a magical global thing.
[00:41:32]
It actually stores all the arguments and the function itself in the virtual DOM.
[00:41:40]
So when it renders a virtual DOM, it knows that this node is something that will be mapped.
[00:41:47]
This node is just plain old HTML.
[00:41:51]
This node is lazy node.
[00:41:53]
Yeah, right.
[00:41:54]
Right.
[00:41:55]
And basically what it does is if this node is lazy, check whether its value has already
[00:42:00]
been computed.
[00:42:01]
If it has a cache in it.
[00:42:03]
If it does, and all of the arguments that are passed, including the function are the
[00:42:08]
same as the one I'm now getting, so nothing changed, then I can just return that value.
[00:42:16]
And I don't have to recompute it again.
[00:42:18]
Right.
[00:42:19]
That makes sense.
[00:42:20]
And otherwise, if something changed or never computed this before, it will compute it.
[00:42:26]
So it will actually call the lazy function during the diff of the virtual DOM, which
[00:42:33]
is way later than I expected.
[00:42:36]
Whereas for the other ones, it's when the view function gets called.
[00:42:40]
But here it's done only when the diff is happening.
[00:42:44]
Right.
[00:42:45]
Interesting.
[00:42:46]
That's really cool.
[00:42:47]
Yeah.
[00:42:48]
And then it sorts that value in the virtual DOM node, the lazy node.
[00:42:52]
And it keeps the whole tree in its model or whatever.
[00:42:59]
Right.
[00:43:00]
So that's a lot cleaner than I expected.
[00:43:02]
Yeah.
[00:43:03]
It's really cool.
[00:43:04]
So do you have a sense of best practices for when and where to use lazy?
[00:43:14]
If there's, like you mentioned, a header or a footer, how much of a difference is that
[00:43:20]
going to make?
[00:43:21]
It's hard to say without benchmarking, right?
[00:43:23]
Yeah.
[00:43:24]
And it's also hard to benchmark that.
[00:43:26]
But do you wait until there's a performance problem to add lazy?
[00:43:30]
Or do you add lazy eagerly or lazily?
[00:43:33]
I guess is what I'm trying to ask.
[00:43:35]
I do it lazily.
[00:43:36]
The only way that it makes sense.
[00:43:41]
Yeah.
[00:43:42]
I rarely use it actually.
[00:43:45]
Maybe because I work mostly with Elm Review where I don't have access to lazy.
[00:43:52]
I use other tricks to do caching.
[00:43:55]
Yeah.
[00:43:56]
Right.
[00:43:57]
The hard way.
[00:43:58]
Yeah.
[00:43:59]
The hard and tricky ways.
[00:44:00]
Yeah.
[00:44:01]
But lazy I rarely use.
[00:44:02]
Where I would use it is more like when I know that I will need to do pretty expensive stuff
[00:44:08]
and things that will rarely change.
[00:44:11]
For instance, if you have a huge list of items and you want to sort them.
[00:44:17]
If the list doesn't change very often, then you can just sprinkle lazy and then you will
[00:44:22]
only do the sorts in the view.
[00:44:25]
Something that I see quite often on the topic is like, oh, where should I sort this?
[00:44:29]
Should I sort it in the updates?
[00:44:31]
Or should I sort it in the view?
[00:44:33]
If you do it in the view and you lazify it, then it's simpler.
[00:44:39]
It's free caching.
[00:44:40]
It's caching that you can't do incorrectly, which should always win.
[00:44:44]
Yeah.
[00:44:45]
Because as we know, there are two difficult problems in computer science.
[00:44:50]
Naming, caching, and off by one errors.
[00:44:53]
Yeah.
[00:44:54]
Exactly.
[00:44:55]
Yeah.
[00:44:56]
No.
[00:44:57]
That's the ideal.
[00:44:59]
You want your values to just trickle through your application without any possibility of
[00:45:04]
getting stale.
[00:45:05]
Yeah.
[00:45:06]
We didn't mention it before, but HTML lazy is very quick in the sense like the comparison
[00:45:12]
with all the arguments is very quick because it does triple equal.
[00:45:16]
By reference.
[00:45:17]
Yeah.
[00:45:18]
Which is very, very fast.
[00:45:19]
Right.
[00:45:20]
So even if it gets defeated, it's not a huge cost.
[00:45:24]
Yeah.
[00:45:25]
But you want to avoid it getting busted.
[00:45:28]
Right.
[00:45:29]
So there's not much maintenance cost or computational overhead to using lazy in a suboptimal spot.
[00:45:39]
Yeah.
[00:45:40]
I think at most it adds 10 comparison.
[00:45:42]
Yeah.
[00:45:43]
Right.
[00:45:44]
Very quick comparison checks.
[00:45:46]
Equal.
[00:45:47]
Yeah.
[00:45:48]
That makes sense.
[00:45:49]
So speaking of comparison.
[00:45:51]
Actually it just might be.
[00:45:52]
Yeah.
[00:45:53]
I think it's one comparison with a list of 10 elements maximum.
[00:45:56]
Yeah.
[00:45:57]
That's what it does.
[00:45:58]
Yeah.
[00:45:59]
Very cool.
[00:46:00]
Yeah.
[00:46:01]
So speaking of comparison, Elm's equality is like, it's so nice to work with.
[00:46:05]
And when you go back to other languages that don't use deep equal by default, you're like,
[00:46:13]
why isn't this equal?
[00:46:16]
These are definitely the same.
[00:46:17]
And you're like, oh, that's right.
[00:46:19]
They're comparing references.
[00:46:20]
And you have to try so hard and use these hacks to check for equality properly.
[00:46:27]
And it's so nice to not think about that in Elm.
[00:46:29]
But that is another place to look for potential performance bottlenecks.
[00:46:33]
If you're doing it over a large set of items doing equality, there is a little bit of a
[00:46:39]
performance cost if you're doing that equality a whole lot or over extremely complex data
[00:46:47]
structures because it is doing it when you do double equal in Elm, it is doing a deep
[00:46:52]
equal.
[00:46:53]
So that's something to be aware of.
[00:46:54]
So I don't know when I'll be done.
[00:46:56]
I am working on an Elm review rule to detect lazy.
[00:46:59]
That would be amazing.
[00:47:01]
And this is really like one of the root rules that I wanted to make for Elm review.
[00:47:06]
Like before I published it, this is really one.
[00:47:10]
I want this one because this is such a non Elm thing.
[00:47:13]
It should not be a problem for us.
[00:47:16]
And we should have a tool to detect it.
[00:47:18]
And that's why I'm making Elm review rule for it.
[00:47:21]
It's a tricky one.
[00:47:22]
It will probably not get it right all the time, but it should help at least.
[00:47:29]
Because you have to do like you were describing in our recent Elm review episode, you have
[00:47:35]
to do flow analysis type stuff to track where references are changing or coming from.
[00:47:42]
Yeah.
[00:47:43]
And the thing is you don't know what will change the reference.
[00:47:47]
For instance, if you call a function, will it change the reference?
[00:47:51]
D doesn't change the reference, list.map does.
[00:47:55]
And you basically need to know what every function does.
[00:47:59]
If you really want to get it right and function from dependencies, I don't have access to
[00:48:04]
search codes.
[00:48:05]
I could, but it would be a lot.
[00:48:08]
It would be very expensive to compute that.
[00:48:11]
Interesting.
[00:48:13]
I will try to get it as right as possible and with as few false positive as possible
[00:48:19]
as usual.
[00:48:21]
Very cool.
[00:48:22]
That's amazing.
[00:48:23]
So maybe it's published already by the time you're hearing this.
[00:48:26]
Oh, yeah, that's true.
[00:48:28]
I'm taking some out today.
[00:48:29]
So don't be surprised if it's not here yet.
[00:48:34]
And I'm being lazy.
[00:48:35]
Yeah, you're either being lazy or you published a lazy package, but lazy somewhere or the
[00:48:41]
other.
[00:48:42]
Yeah.
[00:48:43]
So, okay.
[00:48:44]
So other performance things to think about.
[00:48:48]
So I think, you know, again, like I was saying before, like go back to the basics, think
[00:48:55]
about the platform you're running on.
[00:48:56]
You can't think about performance in a vacuum and we're running code that compiles to JavaScript
[00:49:03]
in a browser.
[00:49:04]
So we have to understand, for example, bundle size is pretty important for initial load
[00:49:10]
performance and Elm has dead code elimination.
[00:49:14]
So I think it's important, even though Elm does this for us under the hood, if you understand
[00:49:22]
what it's doing a little bit, then it might help you take advantage of it more easily.
[00:49:29]
So like basically, I think Mario Rogic was describing it recently saying that it's actually
[00:49:36]
not dead code elimination.
[00:49:39]
It's live code inclusion, which I thought was a nice way to describe it.
[00:49:44]
So like what he means by that is that the Elm compiler lazily pulls in code as needed
[00:49:51]
that you reference.
[00:49:52]
So if code doesn't get referenced, the compiler isn't going to reach it and it's not going
[00:49:58]
to pull that in and compile it.
[00:50:00]
So you start with the main, you look at what it uses, you pull those in, look at what they
[00:50:06]
use, et cetera, et cetera.
[00:50:08]
And when you've reached all the functions that were used, you take all those, you put
[00:50:14]
them in the bundle and you forget all the rest.
[00:50:17]
Right.
[00:50:18]
And Elm doesn't care what you import.
[00:50:21]
If you import something and don't use it, although if you're using Elm review unused,
[00:50:26]
then hopefully you don't have unused imports.
[00:50:29]
But even if you do, Elm doesn't care.
[00:50:30]
It cares the functions that you invoke that are reachable.
[00:50:36]
So Elm does function level dead code elimination.
[00:50:39]
So if you structure things in a way, if you touch a giant record and you only use one
[00:50:46]
field in a giant record, you've just pulled in that giant record to your bundle.
[00:50:51]
Yeah.
[00:50:52]
And I think that's the reason why we tend not to have APIs that expose a record with
[00:50:59]
a to list, from list, blah, blah, blah.
[00:51:01]
Because if you only use one of those, you still get the whole API.
[00:51:06]
Yes, exactly.
[00:51:07]
Yeah, I did that with that in mind in this BCP 47 language tag package I published, which
[00:51:15]
is just basically like a way to use these different language codes with a little bit
[00:51:21]
of type safety, helping you be confident that you're getting the codes correct for languages
[00:51:27]
and countries.
[00:51:28]
And so I don't use any lists or records for it.
[00:51:32]
They're just individual values.
[00:51:34]
And so if there are thousands of codes and you refer to two, then that's what ends up
[00:51:40]
in your bundle.
[00:51:41]
And the fact that Elm has this live code inclusion is that we can put a lot of things in helper
[00:51:49]
packages.
[00:51:50]
Like a very common complaint about the JavaScript ecosystem is about, for instance, lodash,
[00:51:59]
which is very big, but also very useful.
[00:52:02]
But a lot of the functions you will not use in your project, like more than 90%.
[00:52:09]
And we have packages like list, dash extra, dict extra, et cetera.
[00:52:15]
And we can put as many functions as we want in there.
[00:52:18]
It's mostly up to maintenance costs as to what should be in there.
[00:52:23]
But once you add the package and you use one function, while you only use that one function,
[00:52:29]
you don't include all the rest.
[00:52:31]
And that frees us up as library authors to put in as many useful things that we want
[00:52:38]
without having to care about bundle size.
[00:52:41]
And that is very nice.
[00:52:43]
It's really nice.
[00:52:44]
And you see these lodash sub packages that are splitting out separate categories of lodash
[00:52:51]
functions.
[00:52:52]
And I mean, as a user.
[00:52:55]
Every function.
[00:52:56]
Oh, every single function is that?
[00:52:57]
Oh, my gosh.
[00:52:59]
As a user, you don't want the user to need to worry about that.
[00:53:03]
And that's one of the benefits that comes from Elm's purity is that importing a module
[00:53:11]
doesn't have any side effects, running a function doesn't even have any side effects.
[00:53:15]
So you don't need to worry about all of those complexities when you're doing your dead code
[00:53:22]
elimination or your live code inclusion.
[00:53:24]
You can still have side effects when you call a function if it explodes the call stack,
[00:53:29]
but that's about it.
[00:53:30]
Well, that's fair.
[00:53:32]
But at that point, we know how to solve that one.
[00:53:35]
We know how to solve it.
[00:53:36]
And even if the call stack blows up, then Elm can just relax and be like, all right,
[00:53:40]
my job here is done.
[00:53:46]
And we talked about having an awareness of the performance implications of these different
[00:53:55]
techniques and types of code, just so you know what to be on the lookout for if you're
[00:53:59]
looking to optimize something and to look for red flags.
[00:54:04]
I think it's also valuable to understand a little bit about Elm's data structures.
[00:54:10]
And actually, most of the Elm data structures, like if you go to the docs for Elm core dict,
[00:54:18]
for example, then it tells you the complexity of these operations.
[00:54:24]
So it's O log N for insert, remove and querying operations in dict.
[00:54:30]
I think that's really good to know.
[00:54:33]
What does that mean for people who didn't study computer science?
[00:54:36]
Right.
[00:54:37]
I mean, what it means is that there's a relationship between the performance cost of adding, removing
[00:54:47]
or looking up in a dictionary that's related to the size of the number of entries in the
[00:54:54]
dictionary.
[00:54:55]
So the more items there are in the dictionary, the longer it's going to take to remove something.
[00:55:01]
If you say I want to remove something and there are a million things, it's going to
[00:55:04]
take longer than if there were 10 things.
[00:55:06]
Now, it's not linearly related to it where it's going to be a million times slower for
[00:55:13]
a million things than removing one item from a dictionary with one thing.
[00:55:19]
But it means that...
[00:55:20]
That would be the case if it was O of N.
[00:55:22]
That would be the case if it was O of N, exactly.
[00:55:24]
But it's O of log N. And the reason it's log N is because the worst case scenario is it's
[00:55:32]
branching down these branches of a tree so it doesn't have to traverse everything.
[00:55:37]
It can intelligently split the work.
[00:55:40]
And so log of N is way smaller than N for numbers like a million.
[00:55:46]
For numbers like five, obviously it's not going to make much of a difference.
[00:55:50]
But it's important to understand that and just be aware of it.
[00:55:53]
If you're dealing with very large input and you're trying to remove things frequently
[00:55:58]
or add things frequently to a dictionary, that's something you should be aware of.
[00:56:02]
And because I often think of map key value data structures as constant time lookup and
[00:56:09]
insertion and deletion.
[00:56:11]
So I think it's important to realize that it's not for a dict.
[00:56:16]
In a way, in Elm, it feels a bit more expensive.
[00:56:19]
Like if you do dict.get, because the code around it is more complex.
[00:56:24]
You need to do a case of or you need to do maybe map.
[00:56:27]
So it feels a lot more complex than record access or field access in JavaScript, for
[00:56:34]
instance.
[00:56:35]
Yeah.
[00:56:36]
So it's pretty interesting in JavaScript.
[00:56:39]
I recently learned that objects inserting and deleting fields in an object in a JavaScript
[00:56:47]
object makes it like it deoptimizes all of these just in time optimizations that the
[00:56:54]
JavaScript runtime does.
[00:56:55]
Yeah.
[00:56:56]
And also, again, the monomorphic.
[00:56:59]
Yeah.
[00:57:00]
There's this like concept of a shape and it basically calculates where to look for data
[00:57:07]
in an object.
[00:57:09]
But as soon as you change the like prototype by adding or removing things to an object,
[00:57:15]
it can no longer use that reference to quickly figure out where to look things up.
[00:57:19]
And it has to basically recalculate how to look up memory from an object.
[00:57:24]
And anyway, it's surprising the performance characteristics of things are surprising.
[00:57:29]
So even a JavaScript object doesn't necessarily just have like O of one look up time.
[00:57:35]
And a JavaScript map is more optimized for that use case.
[00:57:39]
Yeah.
[00:57:40]
We're going to link to an article called What's Up with Monomorphism?
[00:57:43]
Yeah.
[00:57:44]
Cool.
[00:57:45]
Which is a bit complex, but it explains it well.
[00:57:48]
Yeah.
[00:57:49]
And one thing that is pretty nice that Elm does is that it tries to keep it optimized
[00:57:54]
with this regard.
[00:57:56]
Right.
[00:57:57]
Which is not the case in JavaScript when you write it yourself.
[00:58:02]
Right.
[00:58:03]
Because from Elm 18 to 19, it actually removed the ability to do an object or a record update
[00:58:10]
that changed the shape.
[00:58:11]
Yeah.
[00:58:12]
So Elm Optimize Level 2 is another really great tool that people should check out.
[00:58:17]
And Matt Griffith and Simon Twopp, is that his last name or is that his handle?
[00:58:23]
Either way, they created this really cool tool.
[00:58:26]
And it's a post processing tool that takes the Elm output and it optimizes it using some
[00:58:34]
clever optimization opportunities that Robin found who did a lot of cool optimizations
[00:58:40]
on some of these core data structures like List and Dict.
[00:58:43]
So anyway, that is a nice little free to use tool.
[00:58:49]
You just run it and your performance improves because it tries to optimize the code better
[00:58:54]
for JavaScript engines.
[00:58:56]
Yeah.
[00:58:57]
It will take a few seconds.
[00:58:58]
Yeah, it usually takes around four seconds for me to run.
[00:59:02]
Oh, yeah.
[00:59:03]
So instead of doing Elm dash dash optimize, you do Elm Optimize Level 2 dash dash optimize.
[00:59:09]
Takes a couple seconds longer because it does call Elm make optimize and then does some
[00:59:16]
code transformations.
[00:59:17]
So it's additional work, but it can be very worthwhile.
[00:59:20]
Yeah.
[00:59:21]
But benchmark it.
[00:59:23]
Yeah.
[00:59:24]
And we actually didn't mention Elm Explorations benchmark.
[00:59:28]
Yes.
[00:59:29]
So there's this Elm Explorations package called Benchmark where you can basically create programs
[00:59:36]
or yeah, programs which run functions over and over and over again and compare them.
[00:59:44]
So if you want to know if one function is faster when it's written one way or written
[00:59:50]
another way, then you write them both and then you run a benchmark which compares them
[00:59:57]
and runs it a bunch of times and gives you which one is faster with how likely is it
[01:00:04]
that the results are trustworthy.
[01:00:07]
And it's something that is pretty nice to use.
[01:00:10]
It does make the browser slow, which is annoying.
[01:00:14]
Actually don't even know whether you're supposed not to touch your computer when you're running
[01:00:18]
them.
[01:00:19]
We should mention the browser for the little benchmarking app, not for your actual app.
[01:00:24]
Yeah.
[01:00:26]
I will link to a pull request which I made to this extra where I made a benchmark for
[01:00:34]
an optimization about telecom optimization and you can look at the results there.
[01:00:40]
One thing I haven't found a good way to do yet is using Elm Benchmark is great for comparing
[01:00:49]
two different implementations and for playing around.
[01:00:52]
You can copy an implementation to a parallel module and try some optimizations and figure
[01:01:00]
out if they actually help or hurt.
[01:01:02]
It's really good for that.
[01:01:04]
But for measuring performance over time, it doesn't help you there.
[01:01:08]
I haven't figured out a good process for that because for me, building a markdown parser
[01:01:15]
and some things like this, these are the super performance intensive things where sometimes
[01:01:20]
you do have to sacrifice code maintainability for performance.
[01:01:26]
You need to go for performance because they're so critical for performance.
[01:01:30]
And sometimes you need to add a feature and that will hurt performance.
[01:01:33]
Right.
[01:01:34]
However you change it.
[01:01:36]
Yeah.
[01:01:37]
You need to go that extra effort in a way that often in regular browser applications
[01:01:43]
you don't need to.
[01:01:44]
I would love to have a nice way to track things over time.
[01:01:48]
I guess I really should use this.
[01:01:51]
I know that, so I think it's WebPagetest, if I'm not mistaken, webpagetest.org.
[01:01:58]
I believe you can set that up.
[01:02:00]
And I think maybe Google has a similar thing that you can use through web.dev or something.
[01:02:06]
But I think that it has the ability to track performance over time.
[01:02:10]
I think there are some Netlify plugins that help with this too and maybe some GitHub actions.
[01:02:16]
I'm not sure.
[01:02:17]
I'll add some links to some things in the show notes.
[01:02:20]
Then you can read them and send the results to Dillon.
[01:02:25]
There you go.
[01:02:26]
Yes, that's right.
[01:02:28]
I really should try this out.
[01:02:30]
But I think like webpagetest.org, I think that's a thing that people do is kind of track
[01:02:35]
their Lighthouse scores over time so you can catch any performance degradation.
[01:02:42]
So that could be a helpful thing.
[01:02:44]
That could potentially be something we could use when using maybe benchmarking things.
[01:02:49]
We could create our own little web page that just exercises something and check its performance
[01:02:54]
over time.
[01:02:55]
That will be very expensive compute in CPU time.
[01:03:01]
By the way, so I said when you need to add features, sometimes performance will be degraded.
[01:03:07]
I think I see a lot, especially in the JavaScript ecosystem.
[01:03:13]
There's a new tool which is 10 times as fast as other similar projects.
[01:03:19]
This is React but 10 times faster.
[01:03:22]
When it's new, it's often because they don't have the same feature parity.
[01:03:27]
So like, yeah, you are faster, but you don't handle that one.
[01:03:31]
Are you hinting at ES build?
[01:03:33]
I am not thinking of ES build at all.
[01:03:35]
I actually don't have anything in particular in mind.
[01:03:39]
Very often it's like when you implement the same features that other things have, then
[01:03:46]
you will need to incur big performance costs.
[01:03:49]
So I'm always very wary about things that say, oh, we're so fast.
[01:03:54]
We're so much faster.
[01:03:55]
But you're also kind of new.
[01:03:58]
So beware, beware.
[01:04:01]
Interesting.
[01:04:02]
But if you have the same parity, feature parity, great, awesome.
[01:04:06]
Go for it.
[01:04:07]
Oh, no, yeah, where I often see it is like for linters.
[01:04:13]
There's RSLint, which is ESLint written in Rust.
[01:04:20]
That's what all the cool kids are doing these days.
[01:04:21]
They're rewriting JavaScript tools in Rust or Go, which is probably a good idea.
[01:04:27]
Which is probably a good idea, yeah.
[01:04:29]
And it's so much faster than ESLint.
[01:04:32]
It's great.
[01:04:33]
But it doesn't have custom built rules.
[01:04:35]
So it doesn't have all the rules built by the community.
[01:04:38]
It doesn't have rules that you can build yourself, which is a huge feature in ESLint.
[01:04:45]
And you can't compare the two, in my opinion, without that.
[01:04:50]
And it will be a problem for them to include it into compiled source code.
[01:04:56]
So especially in that instance, I'm like, yeah, no, you're fast, sure.
[01:05:02]
But no, you can't say that.
[01:05:05]
Performance is just so good, though.
[01:05:06]
When you have amazing performance, the experience is incomparable.
[01:05:13]
It just feels so much nicer to use it.
[01:05:15]
So performance is a worthy endeavor, but it is also a never ending challenge.
[01:05:23]
But go for that low hanging fruit, sprinkle in some lazy, run Lighthouse, see what happens.
[01:05:31]
Spend your time focusing on the bottlenecks, not doing random optimizations that make your
[01:05:35]
code hard to maintain, but don't actually impact your critical path for performance.
[01:05:41]
Also serve things up with a CDN.
[01:05:44]
Get that time to first byte down, get the first load performance improved.
[01:05:50]
Get all the low hanging fruit that you can.
[01:05:52]
A lot of the performance will come from web techniques and Elm techniques.
[01:05:59]
Probably the biggest ones.
[01:06:00]
Exactly.
[01:06:01]
Yeah.
[01:06:02]
And that's the other reason you always benchmark.
[01:06:06]
If you make something 100 times faster and it's taking a nanosecond, then that's great.
[01:06:15]
But if you made something 1% faster and it was taking two seconds, then that would have
[01:06:23]
paid for itself much better.
[01:06:25]
So sometimes just adding a little preload directive to preload a font and do that initial
[01:06:32]
handshake or make sure that you're running Terser or some sort of minifier on your Elm
[01:06:41]
output.
[01:06:42]
We'll share a link to the instructions for how to do that in the show notes, both with
[01:06:47]
Elm Optimize Level 2 and with Vanilla Elm.
[01:06:52]
It's not about Elm performance.
[01:06:53]
Elm runs in the browser.
[01:06:54]
So you've got to think about Elm performance, but also the platform it's running in.
[01:07:00]
Feel like we need the more you know, a little sparkling sound whenever we give our public
[01:07:05]
service announcements.
[01:07:07]
All right.
[01:07:10]
Well, I think we've covered performance.
[01:07:13]
All of performance.
[01:07:14]
We also have a whole episode Lighthouse.
[01:07:19]
That's true.
[01:07:20]
Yes.
[01:07:21]
You forgot about it?
[01:07:22]
I've bad.
[01:07:23]
It was a long time ago.
[01:07:25]
Yeah, yeah, that's true.
[01:07:27]
There are a lot of good resources out there on optimizing those details, too.
[01:07:31]
I'll drop a link to a couple of talks about that as well.
[01:07:34]
The Jake and Surma, the Google dev rel guys have some really cool talks where they go
[01:07:41]
into some of these details.
[01:07:42]
So I'll drop a link to a few of those talks.
[01:07:44]
All right.
[01:07:45]
We covered everything now.
[01:07:46]
There you go.
[01:07:47]
Your apps will never have performance issues ever again.
[01:07:51]
Well, until next time.
[01:07:53]
Until next time.