AngularJS: `template` vs `templateUrl`

In the last few months, I have been looking into various ways to improve runtime performance on the giant SPA that I work on at Domo. We have made some serious progress, but with a million lines of code in one SPA, some of the changes aren’t always easy. One of our team members did the discovery to help add lazyloading into AngularJS projects, and we have heavily invested in this. A few different team members (Jason and Tim) dove into helping us measure the time it takes our app to completely initialize. We have also used webpack to streamline the build, as well as change some of the patterns that we use. When combining webpack with ocLazyload, we have found a serious win for AngularJS projects.

This last week, I took on the task of changing all of the component/directive template declarations, and change them from templateUrl to template. Instead of manually moving all of the templates from their separate .html files into their respective JS files, we decided to use a webpack loader, and require the templates as inline strings. In order to better explain it… let me show you what I mean. The following is a sample AngularJS component:

As you can see, in the first example, there is a component that uses a templateUrl to load it’s template. This is problematic at best, IMO. It means that you will need to either deploy the foo/bar/myComponent.html file to production so that your production app can load the template fragment via a second network request to get it, OR it means that you will need to add a build step that will find all of the instances of templateUrl and bring those templates into the AngularJS templateCache. Both of these solutions have problems.

The problems with the first are obvious: if all of your templates in production required a separate network request to get them, then to load any single view would require N network requests to get all of the views, where N is the number of components/directives/ngIncludes in your view.

The problems with the second are that the build steps, while super handy, will load all of your templates into your main webpack bundle. This means that even when you intend to lazyload a component, or an entire section of components, their templates will still be loaded with your main bundle. So, you can’t take full advantage of the benefits you get from lazyloading.

Considering the many hundreds and hundreds of templates that we have in our project, neither of these were feasible. We needed something else. We needed something that would allow us to load our templates efficiently, without separate network requests for each one, while also allowing us to fully lazyload those same templates. So we decided to look at using a webpack loader that would allow us to require our templates into our components as inline strings of HTML/Angular templates.

The Benefits

By using the webpack html-loader to load all .html files, we discovered that we were able to efficiently load our templates, while also allowing us to fully take advantage of the lazyloading. When you use the template: require('foo/bar/my.html') syntax, webpack replaces your require statement with a function that is called and returned with the string for the template. Since the template is now provided as an html string, if you lazyload the component, the template will also be lazyloaded. This is exactly what we needed. However, we discovered several other benefits, the discovery of which prompted this post.

  • Faster Component Initialization — When you use an inline string as a template, the component can initialize synchronously. By using templateUrl, AngularJS will request the template from the templateCache. Since the templateCache may already have the template in it’s cache, or may need to go to across the network to get it, requesting a template from the cache is a process that happens asynchronously. Even if the template is already in the cache, the templateCache will return the already-cached template via a promise based call. This means that the component cannot initialize in the same event loop. The request to the templateCache will always be placed on the next event loop, even in the best of scenarios. This means that the component can begin to initialize, request it’s template, and then finish initializing in the next event loop. But when you use an inline string, the component already has it’s template ready, so it can begin and end it’s initialization in the same event loop. This may not seem significant, but it had several unexpected outcomes that we had to compensate for. 
    - Components initialize faster — which sounds awesome, AIR? Well, it is awesome. However, it means that some of your components that have always had their input values defined when their initialize may break, cause those same values may not be there yet. We had several component break, due to undefined input binding values. We had to change those components to use $watch or $onChanges to detect the update to the input values. 
    - Unit Tests will run differently — Because writing tests change when you are doing a synchronous test vs an asynchronous test, the test for these components may definitely change. For example, in Mocha, if your test is async, you inject the done method into your test, and call it once the test is done. We found that the tests were now performing synchronously, which meant that the need to inject done was no longer necessary. Further, and it is embarrassing to admit this, but we had tests that were written synchronously, however, with the templates being async, those tests HAD NEVER successfully completed. So, when I committed the changes to inline the templates, those tests started to successfully run, and instead of passing, they were FAILING!!!! At first I thought I had broken all of those tests. It wasn’t until after 5 hours of poking around that I realized that these tests were never passing. So we actually now have increased test coverage now that we are using inline templates.
  • html-loader uses an html minifier — This little fact instantly reduced the size of our templates by 19% across the entire app. This is so outstanding, and is really something that we should have been doing for a long time. It also parses the templates, and helped us find a few dozen templates that had invalid html in them. Things like: class"blah", where the = was missing. Or attribute={{something}} , which is missing the quotes around all of it. Once I fixed those, the build worked again.
  • ng-includes were still broken — While the component templates were now working, the ng-include's were now broken. We needed to come up with something for them. So we built a small custom loader, that will bring the template into the templateCache. Our internal practices tell us to not use ng-include, but we still have lots of 3+ year old code that contains them. So, rather than refactor all of that in this commit, I used this new loader, and went to each section of the app that has an ng-include and loaded the template for that section, like I have shown below. This means that the ng-include's are also taken care of in this new process.

Used JSCodeShift

I fully recommend using webpack for AngularJS apps, and using html-loader to bring your templates inline, which means that you will need to change your templateUrl instances to template instances. Since those all look very different, I decided that this was a very good uses case for JSCodeShift, a project from Facebook that allows you to crawl the AST, and programmatically replace all instances for you. You can think of it as Find & Replace On Steroids, Injected w/ More Steroids. It was really simple to write the script that found and updated all of these usages of templateUrl: ‘some/url/to.html with template: require(). I was able to change 90% of the usages programmatically (about 700 files), and I had to finish the last 70 by hand. I could have written the code to finish those other 70, but I figured that I could do those easier by hand than by trying to code them each individually. A quick note, the AST Explorer is an absolute must when using JSCodeShift. Without it, I wouldn’t have been able to make any progress.

Conclusion

Get your AngularJS apps onto a webpack build, and put in the time to get them over to using html-loader to load your templates. Use template instead of templateUrl, and if you haven’t already, stop using ng-include. And then, lazyload, lazyload, lazyload! Some times people distinguish between delayed loading and lazy loading. I am referring to both delayed loading and lazy loading when I say “lazyload”. It is your best change at reducing the time to First Meaningful Paint, and reducing the time to having an app that the user can interact with. Good luck. Back on your heads!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.