Why bother with a CSS build process?

Posted on by ben

Simple web applications can be built quickly, and the original source code sent to users’ browsers for use. While this allows for rapid prototyping and quick turnaround, it’s not a workable model in the long run for a number of reasons:

  1. You are exposing your source code to anyone who may be interested
  2. Raw source code is generally larger than it needs to be (whitespace, comments, long variable names, etc)
  3. A larger project should be split into many source files, and if served directly to the end user separately, this results in a lot of overhead

For Javascript, we use the excellent Closure compiler from Google to combine and minify our code, as well as introduce a modicum of type safety and static code checking.

The build process for CSS often gets much less attention than that for Javascript, since CSS is generally much smaller and less frequently changed. In an application like Lucidchart, though, CSS can be a major part of the application’s download weight, and deserves careful thought.

LESS

The core of our CSS build process is the LESS compiler. LESS gives you the ability to include nested rules, variables, mixins, and even functions in stylesheet source code. Perhaps even more importantly, it provides a system for you to import one stylesheet into another, so that you can separate your styles into short, manageable files.

The LESS website gives great examples of its features and recommended usage. One thing we now do is name all of our LESS “libraries” (stylesheets that exist only to be included in other stylesheets) with a filename starting with an underscore. This is because we’re starting to use Play 2.0 in a number of projects (more on that in a later post), and Play’s build process will automatically compile any .less files that don’t start with an underscore into their own .css file.

Image inlining

One way to optimize a page’s loading time is to minimize the number of requests made to load the page. That means combining multiple stylesheets into one CSS file (which is what LESS is for). Using base64-encoded image URLs in stylesheets can also reduce the number of requests if you have a large number of images.

However, inlining all images can cause its own set of problems. Inlining very large images can dramatically slow down the download time of the CSS file. And if those large images don’t actually need to be loaded immediately (such as an image that’s only used in a dialog that’s seldom opened), it’s time and bandwidth spent for nothing.

The actual script we use to inline images is below. We check URLs that are based at the root as well as URLs that are relative to the location of the CSS files. If the image is less than 5096 bytes (an arbitrary limit that struck a good balance for us), we replace the URL with a base64-encoded data URI.

We also exit with an error code if an image URL is included that can’t be resolved to a file in our webroot. This is a kind of static code checking for CSS, to be sure we don’t have any broken links in our stylesheets.

#!/usr/bin/php -q
\n");

for($i = 1; $i < sizeof($argv); $i++) {
	$content = file_get_contents($argv[$i]);
	$original = $content;

	if(preg_match_all("_url\\([\"']?/([^)\"']+)[\"']?\\)_", $content, $matches)) {
		for($m = 0; $m < sizeof($matches[0]); $m++) {
			$full = $matches[0][$m];
			$uri = $matches[1][$m];
			$fileName = $_ENV["LUCID_HOME"].'/app/webroot/'.$uri;
			if(file_exists($fileName)) {
				$size = getimagesize($fileName);
				if($size !== false) {
					echo "Replacing $full\n";
					$fileContent = file_get_contents($fileName);
					if(strlen($fileContent) < 5096) {
						$replacement = 'url(data:'.$size['mime'].';base64,'.base64_encode($fileContent).')';
						$content = str_replace($full, $replacement, $content);
					}
					else {
						echo "Image $fileName too large\n";
					}
				}
				else {
					echo "Could not get image size.\n";
				}
			}
			else {
				echo "Could not replace $full ($uri => $fileName) in {$argv[$i]}\n";
				exit(1);
			}
		}
	}

	if(preg_match_all("_url\\([\"']?([^)\"']+)[\"']?\\)_", $content, $matches)) {
		for($m = 0; $m < sizeof($matches[0]); $m++) {
			$full = $matches[0][$m];
			$uri = $matches[1][$m];

			if(substr($uri,0,5) == 'data:' || substr($uri,0,1) == '/')
				continue;

			$pathinfo = pathinfo($argv[$i]);
			$fileName = $pathinfo['dirname'].'/'.$uri;
			if(file_exists($fileName)) {
				$size = getimagesize($fileName);
				if($size !== false) {
					echo "Replacing $full\n";
					$fileContent = file_get_contents($fileName);
					if(strlen($fileContent) < 5096) {
						$replacement = 'url(data:'.$size['mime'].';base64,'.base64_encode($fileContent).')';
						$content = str_replace($full, $replacement, $content);
					}
				}
				else {
					echo "Image $fileName too large\n";
				}
			}
			else {
				echo "Could not replace $full ($uri => $fileName) in {$argv[$i]}\n";
				exit(1);
			}
		}
	}

	if($original != $content)
		file_put_contents($argv[$i], $content);
}

Results

We currently have 129 LESS files in our core code repository. Large sections of code are reused across different areas of our application (editor, document list, team management page, etc), resulting in better code maintainability. The final download size is much better than hand-written CSS, and is all included in a single file. We also reduce the number of image downloads by several dozen while only modestly increasing the size of our CSS file.

[Note: Have we piqued your interest in our project? We're hiring!]

This entry was posted in Web Development. Bookmark the permalink.

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>