Using UI Libraries Without the Bloat

I love UI libraries. I still remember the first time I used jQuery UI’s draggable widget. I would type $("*").draggable() in the console for fun (I wrote very practical applications in those days).

But recently, these UI libraries I love have been getting a bad rap for being “huge” and “bloated”. And while I don’t agree with those statements, I can understand where they’re coming from. Advances in the browser have removed some of the need for JavaScript-based interactions altogether. You can even do drag & drop in the browser now (albeit in a very limited manner). And with the ever-increasing importance of mobile, monitoring the number of bytes you send across the wire has never been more important.

But despite all this, UI libraries still provide incredible value for the majority of applications. It’s still shockingly difficult and time consuming to build complex—yet common—Web UIs (e.g. grids, calendars, tooltips). With a UI library, instead of worrying about how to create something complex like a modal dialog, you can write a single line of code, and have an accessible, themeable, and configurable dialog that works in all browsers.

Let’s look at how we can leverage the benefits of these libraries, while keeping our code footprint minimal.

How To Do It Wrong

Let’s suppose you’re building a to-do list with your trendy MVC framework of the month. But your to-do list is special in that it has one additional requirement: the list must be sortable, meaning that users need the ability to rearrange items on the list. After some Googling (or Binging, I won’t judge), you find that Kendo UI has a sortable component and rock out the following code.

<script src="http://cdn.kendostatic.com/2014.1.318/js/jquery.min.js"></script>
<script src="http://cdn.kendostatic.com/2014.1.318/js/kendo.all.min.js"></script>
<ul data-role="sortable">
    <li>Write messaging app</li>
    <li>Get bought by Facebook</li>
    <li>Profit</li>
</ul>
<script>
    kendo.bind( document.body );
</script>

A real to-do list would have actual functionality—but this is a demo, and we’re focusing on the sortable interaction for simplicity.

Bam! With this you get the sortable list below.

But before celebrating, you open your Dev Tools and notice a major problem.

Display of Dev Tools with the download size of Kendo UI highlighted.

Display of Dev Tools with the download size of Kendo UI highlighted.

523 KB… even after gzip? OMG! That will take 12 bajillion seconds to load!

But before you freak out, let’s think about what you’re doing. You are downloading kendo.all.min.js—which is Kendo UI in its entirety. This means you’re downloading the code to build ~30 widgets, handle globalization, create mobile applications, and build over a dozen types of charts, all just to make a list sortable.

Needless to say this is overkill, but how do we just get the pieces we need?

How To Do It Better

The key here is to only download what you need, and most UI libraries give you an easy means of doing this. Kendo UI, jQuery UI, and jQuery Mobile all provide download builders (at http://www.telerik.com/download/custom-download, http://jqueryui.com/download/, and http://jquerymobile.com/download-builder/ respectively).

All three of these download builders work similarly; you check the components that you need and download a modified version of the library. They also have one very important feature: dependency management is built in. In the screenshot below I selected the Sortable component, and its dependencies —”Core” and “Drag & Drop”— were selected automatically.

View of Kendo UI's download builder with the sortable widget checked

View of Kendo UI’s download builder with the sortable widget checked

Yes, this means a little manual work, but the results are drastic. The new version of Kendo UI with only sortable and its dependencies is 23 KB gzipped. A savings of 500 KB!

These download builders are great for quickly configuring downloads, but they become less useful over time. Every time your project’s needs change, you have to return to the download builder, manually check checkboxes, download the custom build, and place it back into your project. A lot of work for us lazy developers! Let’s see how we can make this better.

RequireJS to the Rescue

JavaScript has no native means of managing JavaScript dependencies (although that will be changing soon with ECMASript 6’s modules); therefore we have historically built large applications by placing code in multiple <script> tags communicating between them with global variables.

Even though JavaScript itself has no dependency management mechanism, the Web community has come up with a few different frameworks to meet this need. The most popular of these frameworks is RequireJS, which is an AMD loader. AMD itself defines a way to structure modules that define their dependencies (and we’ll look at that format momentarily).

The good news is that Kendo UI and jQuery Mobile both have complete AMD support, and jQuery UI will shortly (it’s done and will be included in an upcoming release). Let’s look at how using AMD can clean up our dependency management, starting with how we’ll structure our project.

Where to Put All the Files

RequireJS allows for basically any directory structure (see its API for details), but for the purposes of a demo we have to pick one, so we’ll go with the following.

├── index.html
└── js
├── app.js
├── build.js
├── jquery.js
├── kendo-ui
│ ├── kendo.all.js
│ ├── kendo.aspnetmvc.js
│ ├── kendo.autocomplete.js
│ ├── kendo.binder.js
│ ├── kendo.button.js
│ └── [ the rest of the JS files ]
└── require.js

Here jquery.js is version 1.9.1 of jQuery core (as that’s the version Kendo UI’s Q1 2014 release supports); require.js is the latest version of RequireJS from http://requirejs.org/docs/download.html; and build.js is a config file for RequireJS’s optimizer (we’ll get to the optimizer later).

To use these files, we’ll use the following code for our index.html file.

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Get Rich Quick!</title>
</head>
<body>
    <ul data-role="sortable">
        <li>Write messaging app</li>
        <li>Get bought by Facebook</li>
        <li>Profit</li>
    </ul>
    <script src="js/require.js" data-main="js/app"></script>
</body>
</html>

The key line here is the <script> tag that brings in require.js. When require.js loads, it looks for a data-main attribute on its own <script> tag. Here it finds js/app, then asynchronously loads and executes the JavaScript in js/app.js. If you put an alert() in js/app.js, you can verify it is indeed called.

/* app.js */
alert( "OMG! I WAS ASYNCHRONOUSLY LOADED! OMG!" );

But if you try to add our example’s call to kendo.bind(), things won’t work so well.

/* app.js */
kendo.bind( document.body );

When this code executes we get the following error: Uncaught ReferenceError: kendo is not defined. This happens because our index.html file only includes a single <script> tag for require.js. We’re not including Kendo UI—or jQuery for that matter. Let’s look at how we can add the dependencies we need.

Specifying AMD Dependencies

There are two functions used to load dependencies in RequireJS: define() and require(). With define() you create a new module, specify its dependencies, and let RequireJS load those dependencies when the module is used. require() is simpler – it just loads dependencies. For our example we’ll stick with the simpler usage scenario to focus on the basics. Here’s the code we’ll use.

For more on defining modules, Burke Holland has a post on RequireJS fundamentals that covers module creation in great detail.

require([ "jquery", "kendo-ui/kendo.sortable" ], function( $, sortable ) {
    kendo.bind( document.body );
});

require() takes two arguments: an array of dependencies as strings, and a callback to invoke when those dependencies load. In this example, RequireJS starts by asynchronously loading each of our declared dependencies —"jquery" and "kendo-ui/kendo.sortable" — from the network as JavaScript files (jquery.js and kendo-ui/kendo.sortable.js respectively). When those dependencies load, RequireJS invokes the callback function, which in this case invokes kendo.bind().

There’s one other important thing going on. Under the hood, RequireJS not only loads the dependencies we declare, but also the dependencies of those dependencies. For instance, the Chrome DevTools show the following files are downloaded when running our example.

View of Chrome's dev tools showing 7 JS files loaded

View of Chrome’s dev tools showing 7 JS files loaded

This happens because, internally, kendo.sortable declares a dependency on kendo.draganddrop, which itself declares additional dependencies on kendo.core and kendo.userevents. But the great thing about using AMD is that this dependency tree is transparent to us; we just declare a dependency on kendo.sortable and let RequireJS worry about the rest.

Kendo UI core (kendo.core.js) actually adds the window.kendo global variable this example uses. It’s the reason why the kendo.bind() call works.

With this in place, we no longer have to venture out to a download builder to manage our application’s dependencies. For instance, suppose we want to add a calendar to this application. With this setup, all we need to do is add a container to become the calendar.

<div data-role="calendar"></div>

Then add "kendo.calendar" to our list of dependencies.

require([ "jquery", "kendo-ui/kendo.calendar", "kendo-ui/kendo.sortable" ], function( $, calendar, sortable ) {
    kendo.bind( document.body );
});

That’s it! There’s no need visit a download builder and manually check checkboxes.

This is exciting, but with this approach in place we have a new problem: all this dependency resolution happens via additional HTTP requests, and reducing HTTP requests is an extremely important Web performance best practice. Performing six additional HTTP requests can be as slow as downloading all of Kendo UI on some mobile devices, so after all this we’re no better off from where we started.

So how can we get the benefits of managing our dependencies with AMD, and minimize the number of HTTP requests we perform? That’s where RequireJS’ optimizer comes in.

Building for Production With the Optimizer

RequireJS’ optimizer is a Node-based tool designed to optimize front-end resources for production usage. This includes, but is not limited to, combining JS/CSS files, minifying JS/CSS files, and resolving AMD-declared dependencies.

Since the optimizer is a Node.js tool, you must have Node.js installed to use it. If the thought of installing Node.js concerns you, don’t worry as installing Node is now shockingly easy — even on Windows. All you need to do is download the installer from http://nodejs.org/ and run it. That’s it.

To verify things worked, open your command prompt / terminal of choice and type node --version. If you get something other than a “command not found” error, you’re good.

> node --version
v.0.10.17

Node’s install also adds Node’s package manager (npm), which we need to install the optimizer. Run the following to install the optimizer with npm.

> npm install -g requirejs

If you’re on OS X and get a permissions error you may need to run the above command with sudo—i.e. sudo npm install -g requirejs.

With that, you should have the optimizer installed. You can verify this by running r.js from the command line. You should see the following.

> r.js
See https://github.com/jrburke/r.js for usage.

On Windows you may have to run r.js.cmd.

Now we’re ready to build our files. You may recall that we had a build.js file in our example’s js directory. This is the file we’ll use to tell the optimizer how to build our application. We’ll use the following configuration.

({
    baseUrl: ".",
    name: "app",
    out: "app.built.js"
})

While there are plenty of options that you can use with the optimizer, most applications need only a few. Here we’re using baseUrl, name, and out.

The baseUrl controls the base directory that modules should be loaded from. Since our build.js file is in a js directory, it means that when a dependency is specified as "jquery", it is resolved as js/jquery.js.

The name option gives the name of a single AMD module to optimize. Since our application’s code is in js/app.js, we use a name of "app".

For more complex applications that need multiple modules optimized, use the optimizer’s modules option.

Finally the out option controls the filename that the optimizer outputs the built file into. With this configuration in place, all we need to do is run the optimizer. To do so we’ll run the following from the root of our application.

> r.js -o js/build.js

Assuming you have everything setup correctly, you’ll see the following output.

Tracing dependencies for: app
Uglifying file: /myProject/js/app.built.js

/myProject/js/app.built.js


/myProject/js/jquery.js
/myProject/js/kendo-ui/kendo.core.js
/myProject/js/kendo-ui/kendo.userevents.js
/myProject/js/kendo-ui/kendo.draganddrop.js
/myProject/js/kendo-ui/kendo.sortable.js
/myProject/js/app.js

The optimizer starts with js/app.js, finds all of its dependencies, concatenates them all into a single file (js/app.built.js), then minifies the output file. At the end we have a single optimized file we can place back into our index.html.

To use it, let’s return to our application’s <script> tag—which currently looks like this.

<script src="js/require.js" data-main="js/app"></script>

To use the built file, all we need to do is update this tag’s data-main attribute.

<script src="js/require.js" data-main="js/app.built"></script>

To switch between development and production, all you need to do is switch between "js/app" and "js/app.built". You can even automate this with your server-side environment. Burke Holland has a guide on how to do it in .NET.

At the end of the day our app.built.js file that contains all of the JavaScript we need is a mere 55 KB gzipped. Not bad considering 31 KB of that is jQuery core.

For more on the optimizer, including advanced option and usage scenarios, checkout Jim Cowart’s article on Using r.js to Optimize Your RequireJS Project.

Wrapping Up

In general, the most important thing to keep in mind when using UI libraries is to only ship what you are actually using. UI libraries such as Kendo UI, jQuery UI, and jQuery Mobile do a whole lot—and forcing a user to download code they don’t need slows down their experience.

While there are many ways you can build your code – everything from download builders, to manually managing files—AMD provides an elegant solution for dependency management, especially within large applications.

BuildFree_Blog_banner

Comments