Blog

Waldemar Kornewald on August 05, 2010

django-mediagenerator: total asset management

We really weren't posting often enough recently. Now we'll make up for it with an advanced new asset manager called django-mediagenerator. Those of you who used app-engine-patch might still remember a media generator. This one is completely rewritten with a new backend-based architecture and muchos flexibility for the shiny new HTML5 web-apps world (see the feature comparison table). In this post I'll give you a quick intro and after that I'll make another post about some crazy stuff you can do with the media generator.

Why oh why?

What is an asset manager and why do you need one? Primarily, asset managers are tools for combining and compressing your JS and CSS files into bundles, so instead of many small files your website visitors only need to download a single big JS file and a single big CSS file. This is important because request latency has a much bigger impact on your site's load times than file size. You should definitely read Yahoo's Exceptional Performance and Google's Speed pages to learn more about how to improve your site's performance.

The second important task of an asset manager is to help you with handling HTTP caches. This is done by renaming your files, such that they contain a version tag. For example main.css could be renamed to main-efe88bad66a.css. Whenever the file's contents change the version tag is updated, so the browser will not use the cached version of your file, but download the updated file.

Django already has lots of existing solutions for managing your JS and CSS files and images, so why oh why do we make yet another one? Well, they don't provide the flexibility we need:

  • Integration with Sass, PyvaScript, pyjs (the Python->JS compiler used in Pyjamas), etc.
  • Flexible backend system for other converters (CleverCSS, etc.)
  • Versioning for everything (including images)
  • Support for image spriting
  • Uncompressed and uncombined output during development with runserver for easier debugging
  • Works in sandboxed environments like App Engine

Similar to django-compress, django-mediagenerator stores bundles in the file system. The bundles are defined in settings.py. Some people prefer to define bundles in their templates. Why don't we define bundles in templates?

  • It doesn't work in sandboxed hosting environments like App Engine because all files have to be statically pre-generated in advance
  • It can lead to unnecessary bundles and thus slow page loads if different pages have only slightly different scripts
  • The configuration is not very flexible (you can only list a few JS/CSS files)
  • It adds unnecessary checks to every request whether file contents have changed

Even if you'd say that these definitions belong into the templates the disadvantages are much bigger than that little increase in "comfortability".

Let's install it!

We tried our best to make the media generator easy to use while keeping it flexible. On the development server we provide a middleware for serving files. If you want to generate files for production you just run manage.py generatemedia. We also provide two simple template tags for referencing media files in your templates.

So, let's install the media generator. Just download and extract the source code and run setup.py install. Then, go to your project and edit your settings.py:

MIDDLEWARE_CLASSES = (
    # Media middleware has to come first
    'mediagenerator.middleware.MediaMiddleware',
    # ...
)

INSTALLED_APPS = (
    # ...
    'mediagenerator',
)

MEDIA_DEV_MODE = DEBUG
DEV_MEDIA_URL = '/devmedia/'
PRODUCTION_MEDIA_URL = '/media/'

GLOBAL_MEDIA_DIRS = (os.path.join(os.path.dirname(__file__), 'static'),)

It's important that the middleware is the very first middleware in the list. Otherwise media files won't be cached correctly on the development server.

During development via manage.py runserver media is served at DEV_MEDIA_URL by MediaMiddleware. The media generator stores production media in a _generated_media folder. In production, the _generated_media folder should be served from PRODUCTION_MEDIA_URL by your web server. MediaMiddleware is automatically disabled in this case, so Django will not serve any media in production. The MEDIA_DEV_MODE setting specifies whether you're in development or production mode and whether to use development or production URLs (more on that in the templates section).

In the last line we've added the static folder in the project's root directory to the media search path. The media generator will look for media files in that search path. Additionally, all static folders in your INSTALLED_APPS are added to the media search path. Only the admin app is removed, by default. Note that app-specific media should be placed in a subfolder: app/static/app/.... This is very similar to Django templates which are placed in app/templates/app/....

Defining JS and CSS bundles

All required JS and CSS files must be explicitly listed via bundles in settings.py:

MEDIA_BUNDLES = (
    ('main.css',
        'css/reset.css',
        'css/design.css',
    ),
    ('main.js',
        'js/jquery.js',
        'js/jquery.autocomplete.js',
    ),
)

Bundles are defined as tuples where the first entry is the bundle name and the remaining entries list the file names that should be combined. Here, we have a main.css bundle which combines css/reset.css and css/design.css. The second bundle is named main.js and it combines js/jquery.js and js/jquery.autocomplete.js.

Generating media

If you want to generate the media files for production you can just run manage.py generatemedia. This will store all generated files and copy all images into the _generated_media folder. Also, this creates a _generated_media_names.py module which stores the mapping from unversioned file names to versioned file names.

Compressing media

You can tell the media generator to minify your JS and CSS files via YUICompressor in settings.py:

ROOT_MEDIA_FILTERS = {
    'js': 'mediagenerator.filters.yuicompressor.YUICompressor',
    'css': 'mediagenerator.filters.yuicompressor.YUICompressor',
}

YUICOMPRESSOR_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)),
                                  'yuicompressor.jar')

In your project you'll need a convention where to find the YUICompressor jar file. In this case we assume that it's in the project's parent folder and called yuicompressor.jar.

Let's add media to our templates

Now you know how to define bundles, but that's pretty useless if you can't reference those bundles from within your templates. Now this time I'll make it really simple. :)

In your template you first need to add the media generator template library via

{% load media %}

Then you can include JS and CSS directly using e.g.:

<head>
...
{% include_media 'main.css' %}
...
</head>

This will automatically generate <link> and <script> tags. In development mode (MEDIA_DEV_MODE = True) your files are not combined, so this will generate multiple <script> tags instead of a single one. This is very useful because your JS tracebacks will point directly to the file that caused an exception instead of a huge spaghetti code soup file (in that case only grep can save you).

Optionally, you can also specify the CSS media type via:

{% include_media 'main.css' media='screen,print' %}

Image URLs can be generated using e.g.:

<img src="{% media_url 'some/image.png' %}" />

URLs in CSS files

Whenever you write an URL via url(some/relative/path...) in your CSS files the URL gets rewritten to the actual generated file name. This is only done with relative URLs. Absolute URLs stay untouched (e.g., those that start with / or http(s)://).

Example: If you have a CSS file in css/style.css and you want to access img/icon.png you would write url(../img/icon.png).

Sass is treated differently (we'll explain Sass in the next post). In Sass you'd write url(img/icon.png). In other words, you always write the full media path to the file, without the leading DEV_MEDIA_URL/PRODUCTION_MEDIA_URL, of course. Why the difference? When you @import a Sass module we lose all information about the imported code's original location. Thus, we can't support relative URLs like in CSS. In order to make your code easier to understand and more reusable we decided to use full paths, instead.

Web server cache settings

In order to get the maximum possible performance out of the media generator you have to configure proper caching for your _generated_media folder:

  • Disable ETags because they cause unnecessary If-modified-since requests.
  • Use Cache-Control: public, max-age=31536000

This will guarantee that your media files are only fetched once on the first visit. All subsequent requests will retrieve the media files from the browser's cache which will help make your website blazingly fast!

Installation on App Engine

Add the following handlers to your app.yaml:

- url: /media/admin
  static_dir: django/contrib/admin/media/
  expiration: '0'

- url: /media
  static_dir: _generated_media/
  expiration: '365d'

If you use Django-nonrel that's all you need.

If you use some alternative Django setup (app-engine-patch, Django helper, etc.) you'll also need to add this at the top of your main.py handler (or whatever you've called your handler in app.yaml):

import os
if os.environ.get('SERVER_SOFTWARE', '').lower().startswith('devel'):
    try:
        from google.appengine.api.mail_stub import subprocess
        sys.modules['subprocess'] = subprocess
        import inspect
        frame = inspect.currentframe().f_back.f_back.f_back
        old_builtin = frame.f_locals['old_builtin']
        subprocess.buffer = old_builtin['buffer']
    except Exception, e:
        import logging
        logging.warn('Could not add the subprocess module to the sandbox: %s' % e)

This will enable Python's subprocess module which is needed by some media generator backends. Note that Django-nonrel uses a safer solution, but the code above is easier to integrate into the other Django helpers.

How to reload pages utilizing the browser cache

When you press CTRL+R or F5 in your browser all cached files get invalidated and refreshed. This can take a while if you have many media files. Since the media generator automatically takes care of versioning your media files even on the local development server you can use a more efficient technique to reload your pages: Click in the URL bar (or use CTRL+L) in your browser and press enter. That will reload the page without invalidating the cache. That way, only modified files will be reloaded. All other files will be retrieved from the cache.

Update: Current versions of Google Chrome have a bug in the preconnect handling code. Chrome creates multiple connections although the development server is only single-threaded. This causes lockups during reloads because one connection is blocking the development server while the other is trying to communicate with the blocked instance. You can fix this by starting Chrome with --disable-preconnect. This way only one connection is created and the lockups are gone.

Now it's your turn

In the repository you can find a sample project with a little CSS example. If you have installed Django it should work out-of-the-box.

This post should get you started with the most common use-cases. There's a lot more that can be done with the media generator. We'll talk about the really exciting stuff in the next media generator post.

Update: The next post is Using Sass with django-mediagenerator