Minification refers to the process of removing all unnecessary characters from a file while leaving the core functionality of the code in tact. The end result is a new file which is smaller in size to the original, yet identical from a machine perspective. The core benefit to smaller files is that they require less bandwidth and are faster for the client to download. Although not intended, the minification profess can make code more difficult for humans to read, which is why minification can also been seen as light weight obfuscation.
Want to skip the technical discussion in this article? No problem, you can try the latest version of our Minifier.
Before we begin the technical discussion we should explain where and how we use Minification. We use PHP to transfer files from our development environment to production. We opt to automate the minification process during this publication step. This approach gives us all of the benefits of minimization (smaller files which require less bandwidth for the users) without the drawbacks (forcing our developers to work with giant blobs of difficult to read code). Since minification is preformed when a page is published, and not each time a page is accessed, we're less concerned with performance. Thus we opt for readability and ease of debugging over preformance.
At data·yze, minimizing CSS reduces the size of our CSS files by 20-23% on average.
This article is part of a 3 part series.
Let's start with a minimizeCSSsimple() function that will work for most people. The goal of this function is to:
The function looks like
So what's going on here?
Line 3 is the real meat of the function. The function preg_replace() is looking for any string that matches the regular expression, \/\*((?!\*\/).)*\*\/, and removes it from the $css string. \/\* and \*\/ match the /* and */, start and end comment delimiters in CSS respectively. Both /, and * are special characters and need to be escaped. The middle part, ((?!\*\/).)*, preforms a negative look ahead. The expression matches any string (.) so long as the expression \*\/, the escaped CSS end comment delimiter, is not a part of it. Without the negative look ahead, the regular expression would match the start delimiter from the first comment with the last end delimiter from the last comment, rather than the start and end delimiter from the same comment. Any CSS between the first and last comments would be unintentionally removed!
The rest of the code is pretty straight forward. Line 4 collapses white space. Line 5 removes white space where it's not needed, such as before or after a colon, brace or semicolon. Finally Line 6 removes the final semicolon since it's not needed.
If we want to be pedantic, lines 4 - 6 could be merged into a single statement using a 3 element array for all the regular expressions and another for the replacements. I, personally, prefer to have one non-trivial regular expression per line. Keeping lines separate makes the code more readable, which makes it easier to debug and modify. The code only executes when our developers are ready to push changes to production. Since the code isn't client facing, the potential improvement in performance isn't worth the hit to readability.
Let's see it in action with the following example:
The above is test is pretty crazy and test all our corner cases: (1) Two comments on a single line, (2) extra asterisks and slashes in a comment, (3) a necessary white space that shouldn't be removed and (4) a comment that spans multiple lines.Running the CSS through our CSS minified gives us:
As mentioned above, this function works for most cases. It can fail when the content property is set to specific values. Consider the following case:
The regular expression in line 3 interprets the contentproperty of the a::before as the start of a block comment, and the content property of the a::after as the end of the block comment. The following output would be:
In addition to removing comments and condensing white space, we need to preserve the contents of the content property strings. We're going to do that by scanning the string from beginning to end looking for comments and quotes before any minification is preformed. When we encounter a comment, we remove it from the string. When we encounter a content property, we save the value of the string and put a temporary placeholder in it's place.
Next we're going to search for specific patterns, such as the /* ... */ and " .. ". The values represented by the ellipses might change, but each pattern will have a specific start and end string sequence. For this test we write an abstract class MinificationSequenceFinder which will hold the start offset ($start_idx), end offset, ($end_idx) and type, ($type).
Next it's time to implement StringSequenceFinder. This is our class that will find block quotes, (when $start_delimiter = '/*'; and $end_delimiter = '*/'.
The quote sequence finder, QuoteSequenceFinder is a little more complicated. When looking for the end quote of a string we must take care not to use an escaped quote from the middle of the string (e.g. content: "\""). We achieve this on lines 16-19 by verifying the number of preceeding slashes is odd.
The function getNextSpecialSequence() is pretty straight forward. The input variable $sequences is an array of MinificationSequenceFinder and search the CSS string $string for any special sequences. If any such sequences are found, we store the finder in an array $special_idx according to it's offset into the CSS string. The SequenceFinder with the smallest offset (and thus the one occurring first in the string) is returned. We'll use this function again in the next two sections.
Now we can write our code minifyCSS() function.
The while loop in lines 10-20 is where we handle the found strings from MinificaionSequenceFinder instances for comments and content properities. When a comment is found, we jump to switch case /* on line 12, we remove the substring from the CSS string. Since we only have three types of SequenceFinders, the only two sequence finders left is for single quoted and double quoted strings. The default case works for either by replacing the substring with our placeholder from getNextMinificationPlaceholder().
Once all special strings are dealt with, we can use regular expressions to deal with extra whitespace and semicolons. Lines 23-25 come straight from lines 4-5 in our minimizeCSSsimple(). Note we no longer need the line for removing block comments, since that was handled in our while loop.
Finally, we put the special strings back into our CSS with lines 32-34.
We have now replaced our almost good enough 5 line function minimizeCSSsimple() with 123 new lines of PHP. Like I said, it may seem a little over engineered, but at least the output of the new function is now as we'd expect:
Want to give it a try? Use our Minifier.