Owning your Build-step – Owning your Code

Bookmark

Ever since JavaScript has become a language for writing applications, build tools and especially bundlers have been around. They solve the discrepancy between writing code that is easy to maintain and writing code that loads efficiently in a browser. But there are advantages to bundling JavaScript code that go well beyond the browser, from cloud functions to servers to command line tools.


RollupJS is special in that it was always designed from the ground up to be a general purpose bundler rather than a frontend specific tool. In this talk, we will have a look in what way other scenarios can profit from bundling. But more importantly, I will show you how RollupJS not only generates superior output in many situations, but how easy it is to tailor its output to custom requirements and non-standard scenarios. We will see how to patch up code, mock and replace dependencies, elegantly inject build information and control the chunk generation when code-splitting, all with a just few lines of code.



Transcription


So hello and thank you for tuning into my session about how to improve your generated javascript code using rollup. And before I want to go into that part, actually, I want to tackle another question. And the question is like the basic question of javascript. Why are we using all these complex build pipelines and putting all this stuff around our javascript code? And to give you two reasons, I'm going to show you some examples first. So the first example is something we built at our company. It is open source, a small Scrum Poker implementation. You actually might want to give it a try if you want to. But the important part of this, why I chose it is because with this application, it is possible to run it during development without bundling. And I did some measurements like cranking my browser to a really slow network setting for a small mobile phone. And so the times I got is like without bundling, it takes around 18.3 seconds to load. But with bundling, it takes seven and a half seconds. Now I want to say the only change between those numbers was the bundling. So there's actually not a lot less code on the right side. There's no compression, no minification. So why is that actually? Why is there this difference in numbers? And the difference, you can see actually in these two pictures. So it's these green staircase steps here, or it's usually called a waterfall here. So what's happening is it's starting, these are all javascript files, starting with the first file. And then it sees that there's some imports in the file. And once it's discovered those imports, it sends the network request, okay, give me those more files. So it needs to load those files, pass those files and discover some more inputs and so on. And between all those steps here, there's basically network request back and forth, and this is adding up. So of course, what bundling is doing, it avoids the waterfall. But now the question is, okay, this is a web application. I know what we're doing here. So what about our situations? Let's say we are looking at a server. So this time I have a very basic apollo server set up here, just taken from the website. And this time we're not going to give you some made up numbers. This time we're going to do it live. So I'm going to build a server in Docker to give you a reference point here. I have a small terminal here hooked up that is running commands on my machine. So we're going to build a node 14 Alpine image. This is 116 megabytes. Maybe you want to remember this number. And we have two Docker files prepared here. So the first one I'm going to run is a really very traditional setup. So what we are doing is we are copying the package files. We are installing the production dependencies. We are copying the server file. This is this file, and there's a small wrapper here. This wrapper is just there because it starts a small timer, and the timer is stopped once the server says it's fully functional. That's just for measurements. And I'm just running a command that will also immediately start the server to get some time. And what I'm doing here is I'm also copying everything over to another image, just the folder we just created. The reason is that during this year, a lot of caches are created, and this saves another two to three megabytes. So I'm doing this now. This will just take a moment because all of it is cached right now. And the first you see, you remember it was 116 megabytes before. Now it's 134 megabytes, so an 18 megabyte server basically. And startup time was nearly 300 milliseconds. Now I've got a second file here, which is this one. So what this one is doing is it's basically running rollup here, taking the server as an entry point and being naughty, just overriding it again. And there are three plugins, NodeResolve, CommonJS, and JSON, which are necessary for node compatibility. And we are creating CommonJS file, and that's all there is. And then we are basically copying just the created artifact over. And when I'm doing this, let's see what the numbers are now. And you see it's 120 megabytes. So the 18 megabyte server just became a four megabyte server. So why is that? That's because there's really a lot of unnecessary stuff in your node modules. This is typescript types. This is test files, documentation, who knows what unneeded utilities. So this is maybe not as relevant if you say, okay, 130 versus 120, but this was a really basic setup. So this keeps adding up the bigger your server becomes. And of course, startup time was 171 milliseconds. So it's nearly half the startup time. And this is again, the same reason that you saw before, you are reducing the waterfall time. So we are seeing this reduces the size and the startup time. So servers are maybe not that important, but cloud functions definitely are. Those cloud functions really need a quick startup time or also command line tools. So another question, why would you want to use Rollup for this? So there are very good alternative choices. I'm not going to say they are bad because they aren't. What is special about Rollup is that Rollup is designed from the ground up to be agnostic to the target environment. So you can customize it in any way you like. You can build for the browser, for node, for demo, whatever you want. This also means that you usually need to add some plugins to customize it. So by default, it works quite well for the browser, but you definitely need some plugins for node. You also get a nice choice of output formats like CommonJS, ES modules versus various other formats like AMD. And ES modules is actually special because that's Rollup's native format, which means that if you bundle to ES modules and take the output and bundle it again, it just doesn't change anymore. It doesn't get any bigger, which means this is why Rollup is actually used for libraries a lot because here you don't want to have the runtime dependencies all the time. And also we have very good dead code elimination, even though it wasn't important in the previous examples. So I wanted to say we want to actually do stuff with our code. So we know why we want to bundle. The question is of course the how. And what I want to, a pattern I keep seeing here with Rollup is that sometimes, even though they're very good plugins, you basically want to take things into your own hand and you can definitely, you definitely can because it's really easy. So if you don't know what to do during the talk, you could actually join in right now because this is hosted live here. So you can go to lucastagger.github.io slash devops-js. And if you want to start the current slide is hash seven. And if you aren't fast enough to type this, this URL will be on the top of the next two slides. So let's get started here. So the first example, so here's the URL I was talking about. The first example I want to show you is a very common situation. You have a main file here, which is importing information from a file build.js. And in this case, it's just importing a string, which is telling us, okay, this is a development build, but there could be more information here. Let's say there could be a different server during production. So this could be local host during development, but I don't know what server during production or alternative information. And what you want to do is basically you want to have the unchanged set up during development, but during production, we want to change this information. So let's actually write a plugin. So how do you do that? So, and as you can see, my setup is actually running Rollup live while I'm typing. The output is here on the bottom, right? So plugins are an array of objects. And so a plugin exposes several hooks that Rollup can hook into. And the hook we are going to use for the first example is the load hook. The load hook receives an ID, which is an identifier of the file and just usually the path of the file on disk. So all we want to do is if the ID is slash build.js, then we want to return something else. We want to return, so it needs to be export something, export const type equals and maybe use a new line so that you can keep reading what I'm writing. Yeah, production. And here we are, we just wrote our first plugin. So now in the output, it's const type equals production and that's how simple it is. By the way, if you're following this live, again, the URL is here on top. You can just click the button here on the lower left to show you where we want to go to. Okay. So this was loading stuff. Let's go to another example. So this time we're going to invent some syntax we want to use. So let's say we want to have special logging checks and so on during development that you want to remove during production. So the syntax we are making up here is that any time we proceed a line with this comment remove, then we want to remove everything up to the end of the line. The hook we are using this time is the transform hook. The transform hook actually has two arguments. The first argument is the code of the module. The second would be the ID, but our current transformation doesn't depend on the individual module. So how do you do this? Easiest would be actually to use a regular expression. So we're just going to use code replace and replace can have a regular expression as its first argument, which is going to be a global expression. We want to replace all occurrences with an empty string. And what do we need to put in here? It's slash star remove star slash. Okay. You see I already removed my comment, but that's not what I wanted. So we want to remove until the end of the line. So we need several characters, which are not a line break. This is that until the line break. And here we go. So we just removed the second line. I could copy this to the first line. It would also be removed. And also note that this is not just some cosmetic removal of code. This is actually fully integrated into rollups analysis. So if I say I have some constant foo that I'm also including here in the logging, and now I'm adding the comment here, the foo will also be recognized as unused and tree shaking will just get rid of this in our build. Okay. So we've seen two hooks so far, load and transform. To give you an idea how this goes into the wider picture, let's have a high level view on the life cycle of a module. So the situation we have here, we have a module slash main JS, and it just contains for now an import statement. So what does rollup do with this code? The first thing is it will take this import statement and pass it to the resolve ID hook. You didn't see this one so far. We are going to use it in the next example. And the resolve ID hook has two arguments. The first is the import source, exactly as it is written here. And the second is the ID of the importing module. Now this hook is then called on each plugin that implements it until one of the plugins answers it. So in this case, let's say the second plugin says, oh, I know what it means. If someone from slash main JS imports dot slash foo, then they want slash foo.js, which is not surprising. And actually something that core would also have done, because there's a default algorithm that will just do that. Take the directory of the importer and append the relative source here. Okay. So then we need to get the code of the module, and this is done by the load hook. So the ID we just discovered, we are passing now on to the load hook. The load hook will again be checking each of the plugins and the first plugin that implements it and also return something will be the one to give us the code. And since in this example, no plugin implements it, we have a default implementation, which is assume this is the path to a file and try to load it from the file system. So we are getting this code here now. And then there's the transfer mode. Now that we basically have the first glimpse of the code, each plugin has the chance to do some transformations on the code. This is intended for plugins like Babel, like transforming code, but any other code transformation is possible. Again, we have two parameters, the code and the ID, and it goes through all the plugins. The first might be replacing the argument here. The second might be injecting an import. And now that we have another import, it basically starts back from the top and the cycle continues. This is not all plugin hooks that we have enrolled. There's actually quite a few more, but these will be the ones you probably want to use the most. So now let's do another example. So this time we are revisiting our first example. We want to replace some build time information with production information, but this time we want to have two files which already exist on disk. So we could again use the load hook and just read this file in the load hook, but maybe it would be nicer to actually change the file resolution. So each time this file is imported, we want to actually import this file. So it should be something like if ID equals equals equals log minus dev JS, then return slash log minus prod dot JS. Okay. This of course doesn't work because we don't have the ID. We only have source and importer. So what we would like to know is roll up, how would you resolve this source and importer to an ID? And for that, we have context functions. So each plugin hook is called with a special this context containing several functions. This time we want to use this resolve. And since this is an asynchronous function, we want to evade it and it will return an object with an ID property. Okay. Now I'm not going to close this one because as soon as I write the closing parent piece, this will be an infinite loop because calling this resolve will actually call all plugins unless I'm passing a special argument, which is skip self true. And yes. And here we go. So the development build was this, the production build is now logger log and it's now replacing this file with the other one. Okay. Another example. So taking this to the next level means we can actually work completely without the file system. So we are up to now, the IDs were like parts of files, but you're not limited to this. So for the last version of this, we are going to replace information. Just we're going to use a completely virtual file. We call it built out anything around this. So now using what we just learned, we can actually, we need to do two things. So it's already warning here that build cannot be resolved. So first of all, we need to resolve it and resolving is just telling rollup, okay, it's totally fine. This is an ID. So what we're doing is just if source equals equals build, then just return the source again. Okay. And the error changed. Now this time it's loading this and because we are using the browser built here on the presentation, it's complaining about the missing file system. So we're doing the same here. If the ID equals equals build, then just return export. Oh, okay. New line. Const n equals prod. And here we are, our first completely virtual module. And as a very last example of what you can do to basically take full control of your code, patch problems, and do any kind of transformation, like this time we want to do a self-referencing bundle. So we want a bundle that knows what files are in the bundle in the javascript code. And we're using a trick here. So we are importing something from a file, from a file slash build.js. And we're actually not implementing this file right now, but rather we are using this to indicate it's external. And you see already, it is just kept as an import here in the resulting files. Also, you see that they're actually two files generated. The reason is the dynamic import statement here, which rollup interprets as you want to have some lazy loading here. So there will be a different chunk created just for this one. And okay, this is what we're doing here is we are emitting this file. So there's one context helper, emit file, which allows you to add files to the bundle. And an edit file has a type. The type is asset. You could also use chunk, but that has some limitations on how it can be used. So asset is usually what you want for arbitrary files. We need a file name. The file name should be build.js. And we need some source. And now we actually created an empty asset. And because time is running short, we're just using the shortcut here. So what this one's doing is basically taking the bundle object, you get to the generate bundle hook, and it's just listing the names and the size of the file. So right now, main.js is 90 bytes, and this one is 30 bytes. And if I were to change stuff here, you could actually see the number here changing life. So now it's 36 bytes. And wrapping up the talk now, really, there's so much more you could do. So I recommend have a look at this website of rollup telling you how to build your own plugins. And there's actually, if you don't want to go that route, there's also some high level tooling built around rollup. So just to mention a few, there's stencil for web components, VEET, which comes from the vue ecosystem. And it's a very nice development tool without bundling during development with a pre-built rollup step. And WMR is very similar from the Preact community. And for libraries, I can recommend have a look at microbundle, which is also more from the Preact universe for zero configuration libraries, or tsdex for typescript libraries. And with that, I want to conclude my talk. Thank you for staying with me. Just remember, you can access the website locally on your browser if you do this, if you want to play with examples. And thank you very much. Hi, Jan. Nice to be here. Excellent to have you. So let's take a look at the results to your question. What is your experience with rollup plugins? And while... Maybe I overdid it with a number of options. Okay. So if I could have dropped the first questions, I would have expected the majority not having used rollup because it's kind of a niche tool. And I know that the interesting part is further down. Actually, I'm most surprised about the 11% because you can use rollup without plugins, but then it's kind of limiting what you can do. I have great hopes for the 18%. And I was doing the talk mostly for the 18%. Never wrote a plugin, but would consider trying it out because there is like kind of a meta layer for rollup plugins that is emerging. So they're like new tools like VEET and WMR, which are actually adopting rollup's plugin system right now so that you can basically use your plugin knowledge in other tools. Yeah, that's really cool. And lots of love to the lower options, even though it's only 2% each. I have to say that I chose the Chuck Norris option myself, and I see there's many other... Chuck Norris at this conference. So... I know you are. So, okay, let's get to the most important question that we've received so far from the crowd, which is what an amazing terminal for the presentation. What is it? I know the answer, but I'll allow you to tell me. Yeah, the problem is I started a little early preparing for the presentation. And when you have too much time, you start doing things. So I started writing a small web server and put it all into a reveal.js presentation. So basically the terminal is all self-made, but it's also open source. I think so on the last slide of the talk, there is actually the URL for the GitHub repo, if I'm not mistaken, where you can actually look how it's done. It's actually surprising little. So it fits in into one screen of code, the entire terminal on the backend side. So you're what they call an overachiever. Yeah. Very cool. I hope not too many people from my company here watching this, discovering what I did with my time. Okay. So we have a lot of Node attendees at this conference, or I'm just assuming we do. So I guess when bundling for Node with Rollup, are there any important Node features that Rollup doesn't support that you know about and that people should be aware of when using Rollup? Yeah, actually, there are quite a few gotchas and that is kind of important. Like after the talk, it sounds like, oh, it's all super simple, making everything smaller. But so one problem is dynamic requires. This is like a problem that all bundlers have. So when you're writing directly for Node, you can basically require anything like concatenate strings, construct them with functions or whatever. And if there's a module, you can import it, but you can basically not do this while bundling. Also like process CWD. So the current working directory is like not a concept that you have. So this is a problem. So there are a few dependencies that just do not work. We have workarounds for some. So we have actually some nice community contributions so that you have basically a mock Node resolver now in the common JS plugin that you can use for the really tricky cases. And also currently, there's lots of ongoing work with regard to circular dependencies in Node because they also rely very much on inline imports. And yeah, so I hope to solve this in the next months in a good way, hopefully. That's awesome. That's I mean, your work on this project is super appreciated. So those that are the cutting edge among our attendees and maybe starting to use deno, what plugins do you need in order to get that up and running? Yeah, I know like deno is not as used by many, but it's super exciting because like deno has the concept that ES modules are basically the module system you use and which is really native for Rollup. And the other thing is that basically you don't have the old Node modules thing around. So in deno, you use URLs to import your stuff. So all you need for Rollup is basically, okay, you need a typescript plugin if you want to write typescript and you need a plugin for those URL imports. There are actually several like Rollup plugin URL resolve, which will basically just do what deno does for you in your bundle. So I think it would be a very good fit. That's awesome. Okay. So you're saying it's going great with deno. All right. So just to remind you that Lucas will be heading over to the speaker room immediately after the talk and he'll be on the spatial chat and he'll be sticking around on Discord as well. So if you have any further questions, you can definitely drop them for Lucas in the chat or in the spatial chat and speak to him. So thank you so much for being with us, Lucas. Really appreciate you taking the time and this project of yours. Thanks for being here. Yeah. Thank you too.
28 min
01 Jul, 2021

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic