Metro: Scaling JavaScript Build Systems – Miguel Jiménez Esún – JSConf EU 2018


So, hello, everyone. My name is Miguel Jimenez and I work for the
JavaScript Foundation team for Facebook London. And I’m going to be talking to you today about
scaling JavaScript build systems. And more specifically, I’m going to be talking
about Metro. I would like to show you a video about what
the development process of React Native is. For those who don’t know what React Native
is, it’s a framework by Facebook to use JavaScript to create hybrid mobile applications and uses
React as well in order to represent the layout. This is the process. You change things into your editor. We have the iOS and the emulator catching
it immediately. You can change layouts and colors. Those emulators will reflect your changes
almost immediately. And the piece of infrastructure that is powering
off of these interactions is actually Metro. So, to define it in a more formal way, we
can say it’s the development platform for React Native. And does that by exposing an HTTP server so
client, layers can communicate and exposes a web socket server. It can build JavaScript and does other stuff. It deals with assets. When you want to add a photo or video, Metra
adds this. It provides hot loading and it’s extensible
because the main interface is a function that works with Node’s callback. Use that on the Node project or an express
object. And last, but not least, it’s used in the
building process. If you have the app ready and you want to
ship to the building source. Part of the building process whether it’s
X code for iOS or Android Studio, the process of taking your JavaScript application and
putting that inside already done by Metra. Based on what we just explained, it look like
there are a lot of alternatives in the open source world that pretty much fulfill all
of these goals. Why is Facebook building Metro? There are a couple of reasons. We to want make it fast. And by fast, I mean, really fast. Our scale currently is at the tens of thousands
of JavaScript modules. And we’re aiming to be able to perform reloads
on a sub second basis. So, dealing with such a large amount of code
in so small time requires you to have a specific setup that we’ll discuss in a minute. The second thing is we want it to be scalable. You’re probably aware that JavaScript is kind
of a trending language. You can see that based on the audience. So, the amount of times we have at Facebook
just keeps growing. It has to work not only for today’s requirements
for these tens of thousands of JavaScript modules, but it also has to work for tomorrow. And the third reason is we want it to be integrated. So, Metra shaped a set of cool features that
are integrated into the system. And my preferred one to explain this is the
ability of red loading your React Native applications with command RR. The way it works is it brings up a global
hot key listener. This gets inside and pushes toward the emulator,
so they have to reload. This is for developers in most of the web. It gives them a seamless experience. And you need to have specific control on what
your bundler is doing. And last, there’s also a little bit of history. React Native was a project started five years
ago. And back then, there weren’t that many open
source solutions. The first iteration for Metra was JS app server. Which was the React Native packager and we
made it into what Metra is today. Now that we know what Metra is doing and why
we’re building it, I would like to get into the technical details of it. We’re going to do that by covering all the
different processes that are involved in creating a bundle. From the point where you have your code up
until you get your app shipped into the app stores and executed by your customers. So, the first part is about monitoring the
files on a project. As we said, we have quite a large code base
at Facebook. And dealing with such number of files can
be slow. Imagine that every single time you had to
change a file you needed to re traverse your whole dependency graph. If we’re talking about big code bases, this
can take quite a lot of time. So, the way it works inside Metra is by using
the module from a front project, which is Jest. We use Jest haste map. One of the features is it has the ability
of monitoring your file changes and telling you of changes every time it detects something
on your file system. Now, in order to achieve this, it uses Watchman
as much as possible. This is another open source project from Facebook
that monitors the file systems, but as a daemon process. If Metra gets killed and you restart, you
don’t have to query all of it again. You can query Watchman and get the changes
that happened since the last time it was live which dramatically reduces the startup time. However, like all open source projects and
general development, people do not have Watchman installed. If we don’t see it on the file system, we
fall back. This has a startup cost overhead, but after
that, they’re pretty much the same. So, now that we know all the files that are
going to get into our project, the next thing is to transform them. And in that aspect Metra does what any other
bundler will do, which is we use Babel. The thing that is a bit different in Metra
compared to other bundling systems is the way we execute Metra. Transformation is an expensive process and
it can take a lot of time. So, most bundlers will have their main process
and they will execute the transpilation process one after another. Take A and B and so on and so forth. If you have a very large code base, this can
take actually minutes to happen. Metra uses a different approach. Instead the main process doesn’t transform
a file, but spawns child professions called workers. Files are sent to the workers. The transpilation happens at the same time
and then we return the result back. Now, we spawn approximately one worker per
core, meaning a time reduction of 6 7X on a Macbook with eight cores. This cuts building time from minutes to seconds. Which is pretty good. The problem of transpilation has now problems. Imagine worker number one is dealing with
a complex file and it turns out you change again A.JS. The naive approach is taking A and sending
it to the first available worker which turns out to be worker number two. Part of the transpilation process, they tend
to have intermediate caches to make further transpilations faster. If you remember, A was transformed by worker
number one, we set the cache on worker number one. If you send A.JS to worker two, you have the
same cache on worker two. This is bad in terms of space and time. Bad in terms of space. Memory between workers is not shared. You are going to duplicate the same data structure. And bad in terms of time. Because you didn’t have a cache, you are going
to re transpile from scratch. Instead, Metra has a queue for the workers. Once you transpile a file, it’s sent to the
worker. And once it’s returned, it will stick to the
worker that transformed it. If you want to retransform it, put it into
the queue of worker number one and wait for it finish to send to the right worker. This is an oversimplification, it looks like
worker two and three are idling and we could have taken that one. In real life what happens is that worker number
two and three, they’re busy with their own set of files. It is not that we just have one worker super
busy and the rest idling, but the load is actually distributed across all of them. And the module that powers this, once again,
borrowed from another project. It’s just a worker that has a very simple
API to create these kinds of forms of workers. We have an example on the repository about
paralyzing left part. Which is not interesting from the practical
point of view but shows how easy you can do things. Cool. So, we’ve transformed our files and with the
prioritization, it becomes pretty fast. But we want to be faster than this. And in order to achieve this, Metra shapes
with an internal cache. This cache is a multi layer cache and it is
located inside the main process. The way it works with you go into the first
cache layer. If the as a result there, you will return
it. If not, go to the second cache layer and repeat
the process through all the layers. Every time you want to transform something
inside of Metra, you will go through a function called transform. And this transform will got go into the caching
layers. The first is a local cache. This lives inside your laptop and used in
both open source and internally at Facebook. Now, it could be that the thing you’re looking
for is not on that cache. For instance, you just checked out the code
and you have never seen that file, so it was never transpiled. So, for that, we have a second cache layer,
a centralized database. That one is only available at Facebook. But the code is open source, so you could
in theory use that as well. And this centralized cache is accessed by
all developers. We go into the cache and look for the file. Could be the file is not there because it’s
a local change you have just done. The only thing we can do there is go into
the worker and transpile the file. Once the file is transpiled, we put that into
the local cache. But we don’t write to the global cache. And the reason for that is that if we save
every change that every developer is doing through the day, we’re going to end up loading
the cache. And there is no benefit for that. No other person makes the same modification
over the same file and in approximately the same commit. Instead, at Facebook, the centralized cache
is fulfilled by a continuous integration job that runs on master. Which every time we see a new file, or a modified
file and restore the file. In order to restore something, you need a
cache key and a value. The cache key is built by Metra by combining
two parts. We take your source file and we hash it. But we also take every single part involved
into the process of transpiling the file and we also hash it. And the result, the cache that we use, is
the union of both. The cool thing about this is whether if you
change the file itself or change the way you transpile, you don’t have to worry about invalidating
the cache. So, if you didn’t have the second part and
you changed the way you transformed, either you had to invalidate the whole cache, which
can suck for people that is not yet on to that commit, or you would be serving state
results for some time, which is not good either. So, that was for the key. For the value. The value is the result of the transpilation
process. If we take this model, for instance, and we
transpile it, it would end up looking something like that. So, converted into var. It got wrapped into the function. And we got the define coal, et cetera. Now, inside Metra we do not use string identifiers
for modules. Instead we use numeric identifiers. Other bundling systems like Webpack or Browserify
can do that, but in Metra this is built in the core. For performance reasons, it’s faster to do
a lookup through a number. And for size reasons. A number is always smaller than a string. Now, if you cache this, you’re going to have
some issues because these numbers are local to your build. So, while this could work for your local cache,
for the centralized cache when someone else is going to pick that module, they’re built
with crash because left pad could be a complete different module than number 42. So, Metra does not really hard code numbers. It adds one layer of indirection by using
an array of numbers. And each of the modules is changed each of
the reference of the modules is changed into a position into this array. Now, side to the module, we also store that
position number zero is left pad and position number one is five. And when you build, we make a lookup into
the table. We extract which is your local number for
these modules and we add this array to the defined call. Okay. So, we’ve got all the files transpiled and
we’re ready to produce a bundle. Metra produces bundles sorry through something
called serializers where you receive the graph and you can manipulate it any way you want. Now, there are two default serializers shipped
with Metra. The first is a plain JavaScript bundle. When you’re developing in native and open
source, it is the one you’re most likely using. It produces the same format of any other bundling
system. Take one module, one another, concatenate
them and at the end, add a startup code. The startup code is 99% of the time requiring
zero. Because we assign numbers in the look alike
when we traverse the graph and the entry point is almost always the first module. Now, there is a second form which is called
random access module bundles, and this is not a text file, but a binary file. This file is sections. The first one is the magic number. it stands
for something like that. Of the code. Now, after that, we have a table of contents. The table of contents has the amount of modules
that are shipped into this big block. The startup length, remember the startup code
was this require zero. And references to where each of the modules
are located into this giant and after that, we just write the code. Starting with the startup section and each
of the modules. And we put after them a new character and
I’ll explain to you in a second why we do that. Now, the amount of modules let us know how
big the table of contents is. The startup length is how big the initial
length is. And each of the five are there in within the
file. The format might look cumbersome, but thanks
to the table of contents, we can access in real time and where it is located. and this is especially relevant to execute
code on devices. This is very specific to React native as well. And it’s a little bit tricky. I’m going to explain first how we execute,
how we require modules that are previously required and then thousand load a module for
the first time. So, when the module was previously required,
you will have into memory you require implementation and all the files, all the modules that were
previously loaded. Now, when you want to require something, you
will call into the require implementation and the require implementation has its own
internal cache. For instance, because requiring twice the
same module has to return the same instance. It can query the cache. And since the module is there, it will just
return it. Now, this is a pretty straightforward process. And, again, literally every single bundling
system does the same. Now, when you require something that was never
required, the process gets a little bit trickier. We have once again a require and the modules
that were previously loaded and our require call to something that we’ve never seen. The process starts in the same way. We go into require. We make a lookup into the cache. And then 622 in this case is not there. So, we’ve got to load it. And this happens with a little trick. For loading 622 in the case of React Native,
we do not use JavaScript. We use the Native sign. The native sign has native require. Hey, can you load that into the Java virtual
machine? It will go into the disk, look at the gigantic
blob, do the maths, take the first number 622, will extract where it is located and
inject it in the virtual machine. Now, this is the reason why there is a byte
at the end of every module. Because all implementations of JavaScript
built in machines are C++ based, or at least they use ASCIIs and strings. By putting the character at the end of the
module, we don’t have to worry about the length nor coping it into a separate buffer. We can tell the virtual machine to load JavaScript
from there. Once the model is loaded inside of the virtual
machine, it will self register inside the require implementation and it will appear
into the cache. And the only thing that it’s left is the release
the need if require. And now require will be able to return that
module. This process pretty much like the format looks
a little bit cumbersome, but it has some benefits. You have to pay off every single time that
you cross from the JavaScript side into the native side. But on the other side, you are only loading
the minimum amount of JavaScript that you need. And you’re not consuming as much memory as
you would consume otherwise. Loading a plain JavaScript bundle will still
be possible, but it will take a lot of time and a lot of memory. And not all devices are capable to handle
this. You’re only putting inside the inside the
virtual machine the code that you really need to execute. So, we’ve talked a lot about how we bundle
code. How we execute code. A lot of the building process. But what happens with developers? Because we said that we’re looking for sub
second reloads for our developers. So, how have we made this work internally? A couple of months ago, approximately four
months ago, we developed something that is called a Dev bundler. This is open source and it is part of Metra. In order to figure out how that works, let’s
take this graph as an example where your entry point is module number one. And the arrows mean the first one is requiring
the second one. Module number one requires the seconds and
sixth, module two requires three and four, so on and so forth. The first time you want to load this inside
the device you will take all the files, put them all together and send to the device. Now, if you change module number six, but
most do, they will re traverse to number one, get all the dependencies. Create a new bundle and ship that to the client. But the truth is, out of the six modules,
only number six changed. The delta bundler creates a delta just with
that module and sends that into the client. Now, the difference is massive. Because of instead of having an open operation
every time you have a reload, you have an O1. Once it’s loaded on the device, you don’t
have to worry about sending everything over and over through the network. The process works like this for most of the
changes. There are slight modifications when you add
or remove a require. So, when you add a require, we have to extract
that require and start crawling again from that require. And through the through the crawling process,
we only extract the modules, that is the first time we visit. So, for instance, module number one required
number seven. And seven requires four, eight and nine to
work. One was in the virtual machine, so, it will
only contain one, seven, eight and nine. In a similar fashion, when we are move a require,
we have to verify this was the last require going into the file. In that case, module number three stopped
requiring module number five. But module four depends on it. What it means is that the delta that we can
send is only module number three. Now, if we change module number four and remove
the require, five becomes an orphan module and we can safely delete it. And the resulting delta for this is patching
module number four, but also deleting module number five. So, to summarize this, most code changes only
require patching a single file. So, we’ve gone through having to create a
gigantic bundle with megabytes of JavaScript into filling just a few kilobytes. We just triggered that search through all
the new requires that we might visit and take only the ones that is the first time we see. On the other side, removing a require does
the inverse operations. So, instead of depending on direct dependencies,
we depend on what we call inverse dependencies. So, we look at who is depending on a module
in order to know if we can safely delete it or not. If no one is depending on the module, you
can safely delete it. Now, as a quick recap, we can say that Metra
is a building platform where the scaling issues are put on every part of the process. We made transformations in parallel. We have multi cache systems in order to avoid
re transpiling files that someone else transpiled before. And then we have the code execution in the
most optimal way both in development and in production. So, if you want to try Metra, if you’re developing
a React Native application you’re already trying it. All the parts except for the global cache
because we cannot ship the global public cache. All the features I described are in there. And we’re also working into making it for
suitable or other platforms. There are really cool examples about this. This is a video of a React Native application
working on a browser. The thing that is powering this, I would totally
recommend you to check the video. This is just 5 seconds. This is also powered by Metra. And in fact, we’re currently working in making
Metra suitable as well for web. And for that, we have a very simple application
that a colleague of my team made. So, you can just go and download it. Everything works on that app except for the
delta bundler. We are still figuring out how we’re going
implement this on the web. Might be service workers. But this is definitely not there yet. But in any case, it’s actually powered by
Metra. And that was all for today. Thank you very much. [ Applause ]

Tags:, ,

4 Comments

Add a Comment

Your email address will not be published. Required fields are marked *