Archive for the 'performance' Category

Perfplanet.com is open

Friday, June 10th, 2011

#5 This post is part of the Velocity countdown series. Stay tuned for the articles to come.

It's been over a year, since the launch of perfplanet.com. Looks good and useful for people so far. Sergey "ShowSlow" is doing also a great job of tweeting as @perfplanet about news from the perfplanet pipes as well as other interesting happenings in our perf community. Good stuff.

From the beginning I was sure I'm not including all blogs that deserve it. And new ones come up. So I said - send me an email, I'll look around the blog and add it to the planetarium. Problem with that is that the process was cumbersome. I don't always have the time (or am just being lazy because the process is kinda involved). Namely - update a yahoo pipe and update an html page with the list of blogs.

So I decided to take a few hours tonight to remedy the situation. I thought long and hard for what must have been a whole minute and the solution that came was - GitHub.

On GitHub

All the code is now updated, there's a bit of build process, minification and such and the code is now on GitHub, yeey. So if you send a pull request, I just accept it, run the build script and update the site.

Contributing

There is this "planetarium.json" file:
https://github.com/stoyan/perfplanet/blob/master/tools/planetarium.json

All you have to do is update this file, add your (or your favorite) blog for syndication and that's that. Or delete a spammy or irrelevant blog.

When adding a feed URL, try to find the "performance" related category. Because, sadly, not everyone is all that interested in other people's cats, as they are in other people's performance thoughts.

E.g. Ben Cherry's feed is:
http://feeds.feedburner.com/adequatelygood/
But we're only interested in the posts tagged "performance":
http://feeds.feedburner.com/adequatelygood/performance

There are exceptions, of course, some folks only talk about performance.

i18n

About internationalizations - talk to me. There is currently an fr.perfplanet.com, not necessarily maintained. But if you want to aggregate blogs in your language, you can just clone the github project, maintain your blogs and I'll setup your-lang.perfplanet.com and start pulling updates from github.

Build script

So first, I switched from Yahoo Pipes to YQL. Because the aggregation request can be generated from a list of URLs, no need to use a UI, login into Pipes, etc.

Other than that, I added a build script (in JavaScript, yes!) that does this:
- it takes the JSON list of blogs, an HTML template, CSS and JS
- generates index.html with inline minified CSS and JS using cssmin.js and jsmin.js. Gotta minify, gotta save requests
- also in the index.html there's a list of blogger names and URLs generated from the JSON
- generates an up.sh ("up" as in update) - this is a curl call with generated YQL query. This file is executed by a cron job every hour to read new blog posts and write data.js
- data.js is then used in index.html to display the content

Thanks!

So that's that. Hopefully this way the site will see much more updates with new fresh content and blogs.

Bugs, etc, welcome.

Contributions to the list of blogs (or anything else really) more than welcome.

Once again - this is the site and this is its GitHub.

 

Preload in visual search suggestions

Thursday, June 9th, 2011

#6 This post is part of the Velocity countdown series. Stay tuned for the articles to come.

Alrighty, this is something I've talked about last year at HighLoad++ and Fronteers, but never blogged. I came up with this thing while at Yahoo! Search and we used it there in production. So, it must be working. While probably not practical for many sites, the takeaway might very well be: optimizations are all around us, we just have to look around for them.

The summary: browsers have built-in search boxes which also give you search suggestions. IE8+ have added visual search suggestions, meaning images. The trick here is - when you don't have a visual suggestion, why not "suggest" an image for preloading, such as a sprite. Or two.

On to the details.

Chrome search

Every browser these days has them search boxes. Usually in the upper right. Like this:

(Aside rant: I call these browser search boxes "chrome search", but with the advent of the Chrome browser by Google and the fact that Google does search, this is now confusing, even doubly so. So next time you name a project or a company using a name that is already full of meaning such as Chrome or Closure, you're just being unimaginative and.. yeah, not cool. Unless of course it's something like Apple that's too far off from the actual product.)

Chrome search suggestions

Depending on how the search provider was set up, while you type the browser can send the partial string you typed to a URL of the search provider's choice. The search provider then can return a list of suggestions.

BTW, you don't have to be Google or Yahoo in order to be a search provider. This is "open search" and any site could (and frankly, should) be a search provider, here's an example how.

Visual chrome search suggestions

Starting with IE8, the provider can not only offer textual suggestions, but also visual ones, meaning images. So if you look up a stock symbol, it can give you a chart. Or a weather forecast.

The search provider sends back JSON or XML to the browser. The visual thing requires XML actually. Here's more from the horse's mouth. So in order to send an image back you use a syntax like so:

... 
<Item>
  <Text>Currently: Partly Cloudy, 67F</Text>
  <Description>High: 71F Low: 63F</Description>
  <Url>http://weather.yahoo.com/forecast/ USCA1024_f.html</Url>
  <Image source="http://path/to/my/image.png" 
         alt="Partly Cloudy" width="31" height="31"/>
</Item>
...

Preload in visual chrome search suggestions

So what if you don't have an appropriate image for a search term? Or maybe a partial term, say like for example "sch"? Why not load a sprite or some other image, hidden, 0x0 instead. Something that you can be reasonably confident the user will need once they get to the search results page. Easy-peasy:

... 
<Item>
  <Text>schmuck</Text>
  <Url>http://whatever.org</Url>
  <Image source="http://path/to/my/sprite.png" 
         width="0" height="0"/>
</Item>
...

This way you're in effect preloading something that will be needed later. I mean once the user starts typing, there's a pretty decent chance they'll end up on the results page. And they are likely to prefer a fast results page. It will be fast when pieces of it were pre-downloaded.

And if you inspect what's going on in HTTPWatch, you can see that as you type, the browser sends a request to the search provider, which then returns an XML document. And an image, although no image is seen.

What didn't work

I tried preloading CSS and JS too, but it didn't work. These show broken image icons, even with 0x0 size. I also tried 1x1 but then there's a 1x[LINE-HEIGHT] grey square/line.

And that's that

So here - a way to preload images for free, by simply using a browser feature.

You can also use this to preload 1x1 beacons for DNS resolution purposes. Say you serve search suggestions from suggest.example.org but the results are on www.example.org and the static stuff is on static1.example.org, static2.example.org, etc. There's an opportunity to pre-resolve all these domain names by requesting a tiny image from each.

Thanks for reading, this was the second post from the Velocity countdown. Only 5 more days to go! Hope you learned something, even if you don't intend to use it right away. Or maybe at least you've decided to offer your site as a search provider to all browsers. Free marketing, why not?

See you tomorrow.

 

Lazy HTML evaluation

Wednesday, June 8th, 2011

#7 This post is part of the Velocity countdown series. Stay tuned for the articles to come.

Some time ago Google talked about using a sort of lazy JavaScript evaluation which especially helps mobile devices. The idea was to comment out a chunk of JavaScript you don't need right away and serve it this way. Later, when you need it, you get the content of the commented code and eval() it. More here and here.

At the last Fronteers conference I had the pleasure of chatting with Sergey Chikuyonok, who is so great and (among other things) is responsible for coming up with zen coding and writing a bunch of deep articles on image optimization for Smashing Magazine. So he told me he experimented with similar lazy HTML evaluation and it proved to be incredibly helpful for mobile devices. Not only the overall experience is faster but the initial rendering happens sooner and we all know how important that is.

Sergey is a busy person and chances of him writing about his experiment in English seemed pretty low at the time, so I decided to do an experiment on my own and see what happens. Meanwhile he did write about it so I forgot all about my findings, but here they are now.

Long document

I took one big HTML document - The adventures of Sherlock Holmes, which is half a megabyte or about 200K gzipped. Page A is the document as-is, plus some JS for measurements.

Page B (lazy) is the same page but with about 95% of its content commented out. The remaining 5% is a whole chapter so there's plenty of time to deal with the rest while the user is reading. After onload and a 0-timeout I take the commented markup (conveniently placed in <div id="lazy-daze">) and strip the comments. Then take the "unwrapped" time after another 0-timeout to let the browser repaint the DOM and regain control.

The overall skeleton of the lazy page is like so:

<!doctype html>
 
<html>
<body>
  <h1>THE ADVENTURES OF<br/>
  SHERLOCK HOLMES</h1>
  ...
  ... to chat this little matter over with you.</p>
 
  <div id="lazy-daze">
  <!--
    <p>II.</p>
    <p>
    At three o’clock precisely ... 
    ... she has met with considerable success.</p>
  -->
  </div>
 
<script>
 
 
window.onload = function () {
 
    setTimeout(function(){
 
        var daze = document.getElementById('lazy-daze'),
            inner = daze.innerHTML;
 
        daze.innerHTML = inner.substring(4, inner.length - 4);
    
        setTimeout(function(){
            // take end time... 
        }, 0);
                
    }, 0);
};
 
</script>
</body></html>

Experiment

All the files are here:
http://www.phpied.com/files/lazyhtml/

We have the plain normal document - http://www.phpied.com/files/lazyhtml/sherlock-plain.html
And the lazy one - http://www.phpied.com/files/lazyhtml/sherlock-lazy.html

In order to run the experiment you just go to
http://www.phpied.com/files/lazyhtml/start.html
And click "Go nuts". This will load each of the two documents 20 times and take a few time measurements. "Go nuts" again and you'll get 20 more data points.

The time measurements I take are:

  • "plain" - unload to onload of the base version
  • "lazy" - unload to onload of the lazy version NOT including unwrapping it. This should be quicker than the plain version
  • "unwrapped" - unload to onload plus time to unwrap and rerender - this is expected to be bigger than "plain" because the browser has to render twice and is therefore doing more work
  • DOM loaded "plain" - unload to DOMContentLoaded instead of onload
  • DOM loaded "lazy"

Then I take the same 5 measurements but instead of starting at unload of the previous page, it starts at the top of the documents, as soon as a timestamp can be taken with JavaScript. This will exclude DNS, establishing connection, time to first byte...

Results

Here are the results from back when I did the experiment originally last year, using iPhone 2 (with iOS 3.2 or thereabouts)

I ran this experiment over Wifi and again over 3G.

First striking thing - it takes the about the same time to load the plain old page over Wifi and over 3G. For the smaller, "lazy" document, there is a difference, but there's virtually none for the plain base page. The guess here is that the rendering and its cost in terms of memory and CPU is far greater than the actual download time. In other words it takes longer to render than it does to download an HTML. At least in this class of phones. This guess is confirmed when you look at the time from the top of the documents, when the request overhead is removed:

With or without the request time - it's all pretty much the same.

The next striking thing - and how about that lazy document! It renders 3-4 times faster than the whole plain document. Not bad.

And one more surprise - lazy+unwrap time is less than the plain old document. Now that's interesting. It appears faster to split the task into two and do the whole double-rendering, which should've been slower because it's extra work. I guess that poor phone really chokes on the long document.

The same I found is true in Firefox, but almost the difference is negligible.

iPhone 4

I repeated the experiment tonight on iPhone 4 and wifi. And boy, is there a difference. What used to take 13 seconds is now under 3s.

The lazy + unwrap time is more than the plain time, which was to be expected.

Rendering that initial lazy document is still 2-3 times faster that waiting for the whole document.

The numbers:

  • 2765 plain (2014 DOM)
  • 1268 lazy
  • 2995 lazy+unwrap

Ignoring the request overhead:

  • 2200 plain (1421 DOM)
  • 715 lazy
  • 2423 lazy+unwrap

And one last run/observation - on the 3G and iPhone 4 there isn't much benefit of lazy-evaluation and empty cache. The request seems much more expensive. unload to onload 4.9s where document top to onload is 2.5. When the request overhead is out of the picture than lazy eval wins again - 1.7s compared to 2.5s

Parting words

  • Lazy HTML FTW?
  • Who the heck loads an entire book in a page?! Well it may happen. It may not be a whole book, but just a lot of markup. The entire book gzipped was 219K. A hefty document, but have you seen some of those news sites?
  • Possible use case - blog comments. Lots and lots of blog comments. Or posts.
  • If you're going to lazy-load something and get it with an ajax request, why not save yourself the request and ship with another chunk of html
  • This was a simple layout task. Just a bunch of text. I'm guessing there could be much more complicated pages and layouts to render. And rendering is what takes the time it seems.
  • Drawbacks a plenty because of the hidden content - accessibility, SEO.

Thoughts? Anyone want to run the test on Android or any other phone/device/tab/pad/whathaveyou? The guess is that the newer/powerful the device the smaller the difference. But it will be nice to know.

 

Velocity countdown

Wednesday, June 8th, 2011

Let me try to set the mood for Velocity 2011 with an attempt to publish one performance-y post a day for the seven days between now and when the conference starts.

#7 - Lazy HTML evaluation
#6 - Preload in visual search suggestions
#5 - perfplanet.com is open
#4 - YSlow 2.0: the first sketches
#3 - Book of Speed
#2 - Sultans of Speed
#1 - Overlooked Optimizations: Images - guest post by Billy Hoffman

Tada! Velocity is here!

 

Simple Sharding Logic

Friday, May 6th, 2011

Sharding is the technique where you split static components across multiple domains so the browser can fetch more components in parallel? (Which browser? How many components per hostname? Ask browserscope.org)

So you have, say, image-a.png and image-b.png. You want to serve image-a from domain1.example.org and image-b.png from domain2.example.org

The thing is you don't want to randomize which image goes to which host in different page views. Otherwise you'll do like cnn.com where the same image (a 1x1 gif) is served from three different domains (in the same pageview even) causing three HTTP requests.

Here's a simple solution: use modulus to split components into buckets based on their URL (or path name).

Billy "Zoompf" Hoffman mentioned this as a simple strategy - if you want to split to two hostnames, take the length of the image path. If it divides by two - serve from host 2. Otherwise - host 1.

Same simple logic can apply for splitting into as many hostname buckets as you want. Need three buckets? % 3. Four buckets? % 4.

You can even do it per browser, for example if it's IE6, split to more buckets.

Here's the SSL (Simple Sharding Logic :) ) expressed in JavaScript:

function getBucket(url, numbuckets) {
  var number = url.length,
       group = number % numbuckets;
  return group;
}

If you think most of your paths will have same length, e.g. /images/top.png, /images/nav.png, /images/bot.png you can use some other number, e.g. the character code of the middle letter in the path.

var number = url.charCodeAt(parseInt(url.length/2));

Or the code of the letter in 1/4 of the path + the one in the middle + the on in 3/4 of the path. You get the point - all you need is a number that can be produced from the file path (or content or anything) and won't change from one page view to the next. You need stable file-to-hostname resolutions above all.

You can also pass an array of components and get a multi-array of the components, grouped into buckets. Here's what I tried on cnn.com and worked beautifully:

function toBuckets(stuff, numbuckets) {
  var numbuckets = parseInt(numbuckets, 10),
      url, group,
      buckets = Array(numbuckets),
      cache = {};
  for (var i = 0, max = stuff.length; i < max; i++) {
    url = stuff[i].src;
 
    if (typeof cache[url] === 'number') {
      continue;
    } 
    group = getBucket(url, numbuckets);
    if (!buckets[group]) {
      buckets[group] = [];
    }
    buckets[group].push(url);
    cache[url] = group;
  }
  return buckets;
}
 
console.log(toBuckets(document.images, 3));

This gave me (on cnn.com) a nice list of three buckets and the URLs in each:

[
  ["http://i2.cdn.turner.co...seals.02.cnn%5B1%5D.jpg", 
   "http://i2.cdn.turner.co...ves/tzvids.osama.gi.jpg", 
   "http://i.cdn.turner.com.../misc/advertisement.gif", 5 more...], 
  ["http://i.cdn.turner.com...der/hat/arrow_black.png",
   "http://i2.cdn.turner.co...ds.military.dogs.gi.jpg",
   "http://i2.cdn.turner.co...bl.house.cnn.120x68.jpg", 8 more...], 
  ["http://i.cdn.turner.com...element/img/3.0/1px.gif",
   "http://i.cdn.turner.com...bal/header/hdr-main.gif", 
   "http://i.cdn.turner.com...al/header/nav-arrow.gif", 9 more...]
]

Not too bad for such simplicity.

As you see, I have a cache object, so it takes care of any duplicates so I don't end up with the same component repeating.

The drawback of course is that it's unlikely that you'll get exactly the same number of components in each bucket. But from what I tried on a few sites, that's not a big issue. The benefit of having a stable and simple (no databases, cookies, local storage, etc) resolution of file path to a hostname is a win.

 

Audio sprites

Wednesday, April 13th, 2011

Another "brilliant" idea that I had recently - how about combining audio files into a single file to reduce HTTP requests, just like we do with CSS sprites? Then use the audio APIs to play only selected parts of the audio. Unlike pretty much all brilliant ideas I have, I decided to search for this one before I dive in. Turned out Remy Sharp has already talked about this. So I knew it was possible and wanted to check the server-side or things. (Remy is amazing, by the way, and I was happy to have him as a reviewer of "JavaScript Patterns")

Here's the demo - parts of Voodoo Chile covered by yours truly.

Playing separate files

Markup is a few audio elements:

<audio id="in">
  <source src="in.mp3">
  <source src="in.ogg" type="video/ogg">
</audio>
<audio id="1">
  <source src="1.mp3">
  <source src="1.ogg" type="video/ogg">
</audio>
<audio>
  ...

I have the files:

  1. in.mp3 - intro
  2. 1.mp3 - figure 1
  3. 2.mp3 - figure 2
  4. out.mp3 - the end

1 and 2 repeat a few times.

I play these files in JavaScript using a next() iterator function, which contains (in a private closure) the melody (which file after which) and a pointer to the current file being played. After play()-ing each audio, I subscribe to the "ended" event and play the next() file.

var thing = 'the thing';
var next = (function() {  
    //log('#: file');
    //log('-------');
    var these = ['in', '1', '2', '1', '2', '1', '2', '1', 'out'],
        current = 0;
    return function() {
        thing = document.getElementById(these[current]);
        //log(current + ': ' + these[current] + ' ' + thing.currentSrc);
        thing.play();
        
        if (current < these.length - 1) {
            thing.addEventListener('ended', next, false);
            current++;
        } else {
            current = 0;
        }        
    }
}());

That was easy enough. And worked (except on iPhone, see later). But we should be able to do better with sprites.

Sprite

For the sprite I have this audio:

<audio id="sprite">
  <source src="combo.mp3">
  <source src="combo.ogg" type="video/ogg">
</audio>

There's only one file - combo.mp3 which contains all the other four files played one after the other.

So we need to know the start and the length of each piece of audio. There are two parts to playing the sprite. First is knowing the lenghts and the "song" (meaning the succession of audios) and starting to play:

var sprites = {
      // id: [start, length]
       'in': [0, 3.07],
        '1': [3.07,  2.68],
        '2': [3.07 + 2.68,  2.68],
        out: [3.07 + 2.68 + 2.68, 11.79]
    },
    song = ['in', '1', '2', '1', '2', '1', '2', '1', 'out'],
    current = 0,
    id = song[current],
    start = 0,
    end = sprites[id][1],
    interval;
 
thing = document.getElementById('sprite');
thing.play();

Next is "listening" and stopping when one audio should be stopped, then seeking through the file and playing another part of it. This is done with a setInterval(), I couldn't find a better audio event to listen to.

// change
interval = setInterval(function() {
    if (thing.currentTime > end) {
        thing.pause();
        if (current === song.length - 1) {
            clearInterval(interval);
            return;
        }
        current++;
        
        id = song[current];
        start = sprites[id][0];
        end = start + sprites[id][1]
        thing.currentTime = start;
        thing.play();
        log(id + ': start: ' + sprites[id].join(', length: '));
    }
}, 10);

And this is it. The property currentTime is read/write - you can figure out where we are and also fast-forward or rewind to where you want to go.

Results

  • Sprites play fine in FF, Chrome, O, Safari, iPhone's mobile webkit.
  • I haven't tested IE9.
  • All the browsers played y stuff off by a few milliseconds, I think some early, some late. This is probably due to unreliable setTimeout(). Also I didn't cut the audio pieces very well, so that might have someting to do. Also adding a few milliseconds of silence between the sprites may help. A follow up experiment will be to have a piano of sorts and see how timely the audio is played after a click/button press.
  • iPhone didn't play properly the non-sprited verison - I believe because it won't let you autoplay unless there's a user action. There might be a workaround, but I only cared about the sprites and they are fine!

Server side

I was imagining the whole thing as a combo service like YUI's JS/CSS combo handler. The browser says: i need these 5 files, the server then creates a new audio file and sends it back. In this case it should also somehow send the data about start/length of each audio, so maybe a JSONP thing. I was mostly curious about those file formats and how the stitching would work.

In terms of file formats, it's not that bad, turns out all I need is MP3 and OGG in order to support all these browsers (I was prepared for worse).

(I could also probably support IE3? and above with a <bgsound> and a WAV, but the WAV is too big to be practical. So any IE (before 9) enthusiasm should probably end up in Flash.)

I recorded my audio pieces in Garage Band and exported as MP3.

ffmpeg is teh tool! It's like imagemagick for audio/video.

Cutting out extra 4-5 seconds Garage Band adds to each file you export:

$ ffmpeg -i in.mp3 -ss 0 -t 2.43 in-ok.mp3

(I didn't do that very precisely I think)

Converting MP3 to OGG is like:

$ ffmpeg -i in.mp3 -acodec vorbis -aq 60 -strict experimental in.ogg

Then the stitching.

MP3 files can actually be concatenated together just like JS/CSS, provided they have the same bitrate. I've done it in the past.

You can also combine by reading the files and piping them into ffmpeg. That somehow feels better:

$ cat in.mp3 1.mp3 2.mp3 out.mp3 | ffmpeg -i - combo.mp3

You can also consider putting a bit of silence between the separate audios.

In order to get the length info to return it to the client, you can use ffmpeg -i filename.mp3 (I haven't done that part)

OGGs cannot be concatenated like MP3, so the combo service should `cat` the mp3s as shown above, then convert to OGG (also shown :) )

Voila

You can now roll your own on-demand audio combo handler and use audio sprites to have fewer HTTP requests a more responsive app/game/html5 thing.

 

A rounded corner

Wednesday, April 13th, 2011

Warning: not practical blog post, don't read, move on.

So this is a post about a thought I had - creating rounded corners in IE678 by using roundness that they already have built-in, meaning the character O.

But first:

  1. My opinion is that browsers that don't support border-radius should never ever get rounded corners. Let them rest in peace.
  2. Don't use the technique below, it's just a thought. Plus it only has one corner

Demo

Live page demo and a screenshot in IE6:

IE rounded corner

Sales pitch

  • Rounded corners in IE 6, 7, 8
  • No images
  • No JavaScript (a tiny self-rewriting CSS expression doesn't count)
  • No extra markup

The drawbacks later.

The big idea

Use the letter O in monospace and position it in the corner.

Implementation

Markup

As promised, nothing to see here:

<p>Hello world</p>

JavaScript

None. *

* The catch here is that a tiny piece of JavaScript exists as a CSS expression. It's there to trade for clean markup. You can remove it, but then you need a bit of extra markup.

CSS

p {
  /* blah-blah, border, padding... */
  border-radius: 16px; /* for good browsers */
  background-image: expression(...); /* IE[678] */
}

The expression goes like:

this.runtimeStyle.backgroundImage="none",this.innerHTML += "<b>O</b>"

(Expression stolen from Thierry, btw)

The first part of the expression overwrites itself for performance reasons. You know that expressions are bad, cause they execute too often. Well, this.runtimeStyle shuts down the expression. If you wonder, runtimeStyle is IE thing which makes styles even more specific than inline style attributes. And this refers to the HTML element, in our case the P.

The second part of the expression (note the , separator, that's kinda funny) updates the innerHTML of the P adding a B element. So the end result of running the expression at initial page load is DOM like:

<p>Hello world<b>O</b></p>

And if you prefer, you can put that markup and get rid of the CSS expression.

The rest of the CSS is just wrestling to position the O in the corner:

b {
  background: white;
  display: block;
  font-family: monospace;
  font-size: 72px;
  font-weight: bold;
  height: 41px;
  left: -18px;
  overflow: hidden;
  position: relative;
  top: -74px;
  width: 25px;
}

And this is it.

Drawbacks

I'm sure my critical readers can think of drawbacks but let me start:

  • In general principle, why would you care about rounded corners in browsers that don't know about border-radius?
  • You can't have different background inside and outside the box, because you can't style the inside of the O with a color different than the outside. However you might be able to find a character that can.
  • Playing with font sizes and positions is tricky. However there's probably a better way to position the O
  • If you managed to select text outside the "hello world" (if you do Select-All for example) to copy, you'll paste "hello worldO" :) Which is exactly what screen readers will read and your page might sound like a weirdO

So there

Maybe someone else have already thought of the idea of using a character as a corner (and has a better implementation), but that's all from me. I'm not recommending this approach, just an itch I needed to put out there. Thanks for reading!

 

HTTPWatch automation with JavaScript

Sunday, April 10th, 2011

Background

I gave this short presentation at the recent Yahoo FE summit's open mic, here are the slides and some notes.

Slides and screencast vid

Screencast to see the thing in motion:

Notes

Here's the transcript of the slides as produced by slideshare.net. I've added some notes here and there to make it more readable when the presenter is missing.

JavaScript shell scripting - Presentation Transcript

  1. JavaScript is everywhere #42:
          C:> WSH
  2. Stoyan

    I do programming.

  3. Programming

    There are many options to choose from when you decide top practice the
    art and craft of programming.

  4. JavaScript

    ... is a very good one. Simply because JavaScript...

  5. ... is everywhere
  6. On the server

    node.js, asp.net

  7. Mobile

    html5, phonegap, titanium

  8. Desktop

    XULRunner lets you create cross-OS GUI apps

  9. Browser extensions

    FF, Chrome, bookmarklets...

  10. Photoshop

    yep, that too
    Several Adobe products actually let you script common/uncommon/programmable tasks

  11. Form validation too!

    (this was supposed to be funny)

  12. Shell scripting

    let's talk about shell scripting with JavaScript

  13. In Windows
  14. WSH: Windows Scripting Host

    All reasonable Windows machines (at least as old as Win2000) have this Windows Scripting Host in there.
    You can write VBScript or JavaScript (OK, JScript) to ... well, script.
    How does it work?

  15. C:>edit hello.js

    You create a file.

  16. var a = "Hello",
        b = " WSH!",
        c = a + b;
            
    WScript.Echo(c);

    Put any old JavaScript in there and print out some results

  17. C:>cscript hello.js
    Hello WSH!

    And this is how you run it.

    Or this:

    C:>wscript hello.js
  18. Open apps

    In addition to regular sysadmin tasks (copy, write files, move) you can open and script applications too.

  19. var ie = new ActiveXObject("InternetExplorer.Application");
    ie.Visible = true;
    ie.navigate(yahoo.com);

    This is how you open IE and point it to a page.

    Notice something familiar? ActiveXObject - the thing we used in IE before it got native XMLHttpRequest

  20. Firefox?

    Can you also open FF?

    Not really, as it doesn't have COM interface (whatever that is).

    But there's an easy solution

  21. HTTPWatch

    Finally we come to the topic of the talk.

  22. Speed

    Performance is critical for the success of any web app.

    Really, it is.

    When talking about improving speed there are two main steps:

  23. 1. Fix with YSlow

    Take a slow page, run YSlow, do what it says.

    Voila - a fast(er) page.

  24. 2. No regressions

    The second step is to not allow regressions.

    Whatever you fix in step 1 will be slow in the next few months.

    Even less than months the bigger the team or the rate of changes.

    So to prevent regressions, you need to constantly...

  25. Monitor
  26. Set limits

    The simplest way to prevent regressions is to set some limits.

    If you go over the limits, an email is sent, an alarm sounds, panic instills and you've got to fix whatever cause it was.

  27. e.g.
    max 2 scripts
    max 2 styles
    max 9 images
    max 0 redirects
  28. Scripting HTTPWatch

    Watching for violations of the limits manually every day is not a job anyone would want.

    So automating it will help a great way towards employee satisfaction :)

  29. var http = new ActiveXObject("HTTPWatch.Controller"),
        ie = http.IE.New(),
        ff = http.Firefox.New();

    This is how you open IE and FF with HTTPWatch's help.

    FF - yey!

  30. // browser cache
    ie.clearCache();
    
    // show HTTPWatch
    ie.OpenWindow(false);

    Examples of stuff you can do with the HTTPWatch API.

    You can for example hit the page with empty cache and then again with full cache.

    Best of all - these are the real browsers with their sometimes kinky behaviors.
    Actually if you setup several machines for the monitoring (or somehow do multiple IEs)
    you can test with different versions of the browsers. Nothing synthetic!

  31. ie.Record();
    ie.GotoUrl("yahoo.com");
    http.Wait(ie, -1);
    ie.Stop();

    Start monitoring, go to a page, stop monitoring after the page "settles" meaning some time after onload.

    ie.CloseBrowser();
  32. new HTTPWatch()
          http://github.com/stoyan/etc/

    I did this JavaScript thingie to make everything a little easier.

  33. var http = new HTTPWatch(ff);
    http.go(search.yahoo.com);
    http.done();

    Example usage.

  34. [video]
  35. var har = http.toHAR();
    har = eval(( + har + ));
    
    print(har.log.browser.name);
    print(har.log.browser.version);
    print(# requests: );
    print(har.log.entries.length);

    Opening and navigating browsers is cool. But we need some data back.

    HTTPWatch can export a HAR (HTTP Archive) file. I have this toHAR() method.
    It writes the file, than reads and returns it.
    You can than eval() it because it's just a JSON string.
    And you get the data back in convenient JS objects and arrays.

  36. Internet Explorer 6.0.29...
    # requests: 10
    
    Firefox 3.5.6
    # requests: 15

    Result of running the above.

  37. [video]
  38. var comps = http.getComponentsByType();
          
    for (var i in comps) {
      print(i);
      print(comps[i].length);
    }

    Another method I thought would be useful is getComponentsByType()

  39. redirect: 1
    text/html: 3
    image/gif: 4
    image/png: 3
    text/javascript: 1

    Results of the code above.

  40. But wait...

    There's more :)

  41. What about DOM?

    So far we only talked about HTTP traffic inspection - headers and such.

    Good news is that you can also inspect the DOM (in IE only) for any potential red flags.

    For example having the number of DOM elements sharply increase.

  42. [video]
  43. var http = new HTTPWatch();
    http.go(search.yahoo.com);
    
    var d = http.watch.container.document;
    
    print(d.getElementsByTagName(*).length);
    print(d.documentElement.innerHTML);

    That works!

    All your DOM voodoo skillz are suddenly reusable.

  44. require(statz.js);
          
    var doc = http.watch.container.document;
    var html = http.har.log.entries[0].response.content.text;
    
    var out = statz(document, html);
    print(out.join("\n"));

    This is me repurposing two old bookmarklets that gather some interesting stats (one of them was even featured on Ajaxian, remember?).

    It was pretty easy to repurpose the bookmarklets, because it's just JavaScript.

    The stats thingie can inspect both raw HTML that went over the wire, as well as innerHTML that was the result of any additional DOM manipulations.

  45. JS attributes (e.g. onclick): 1207 bytes
    CSS style attributes: 883
    Inline JS: 5243
    Inline CSS: 5015
    All innerHTML: 17283
    # DOM elements: 134
    
    Total size: 14124 bytes
    Content size: 401 bytes
    Content-to-markup ratio: 0.03

    Sample results.

  46. To summarize...
  47. JavaScript
    WSH
    HTTPWatch
    Monitor
    DOM and HTTP
    IE and Firefox
  48. Thanks
          
    phpied.com
 

YSlow/Chrome hacking

Thursday, March 24th, 2011

If you haven't seen it yet, YSlow for Chrome hit the streets couple of weeks ago. (And Google's own PageSpeed did too yesterday. (And there's now DynaTrace for Firefox. (And WebPageTest for Chrome. (What a month for x-browsering (word?) the performance tools! (And the month's not even over yet)))))

BTW, I closed all the parentheses (or else...).

So anyway, I was eavesdropping on a twitter conversation where Sergey (of ShowSlow) was asking for beacons from YSlow for Chrome, more specifically - when will they start working. I thought I should check how my old baby YSlow 2.0 (this presentation is still pretty relevant) is doing in its new environment.

YSlow 2.0

In YSlow 2.0 things are pretty decoupled. Makes it easier to bring to any possible environment or browser. So rules are rules (you can add, remove, tweak them, combine them into rulesets), results are results, presentation is separate, and so are the additional tools, HAR import/export (forthcoming), etc. Only (ideally) small additions are needed to glue the core of YSlow (the linting part) with a new environment.

In Chrome

It's my first time touching anything Chrome-y, but turned out its pretty easy. Just a bit of file system hunting revealed where code for the extension goes.

/Users/[USERNAME]/Library/Application Support/Google/Chrome/Default/Extensions/[WEIRD-EXTENSION-ID]/

e.g.

/Users/stoyanstefanov/Library/Application Support/Google/Chrome/Default/Extensions/ninejjcohidippngpapiilnmkgllmakh/

On Windows:

C:\Documents and Settings\[USERNAME]\Local Settings\Application Data\Google\Chrome\User Data\Default\Extensions\ninejjcohidippngpapiilnmkgllmakh\

In there you can find three JavaScript files. I could be wrong but here's what I think goes in there:

  1. yslow-chrome.js looks like it contains the reusable parts - rules, etc - packaged for Chrome minus the Firefox stuff and bundled into a single file
  2. content.js is small and not too exciting
  3. controller.js is the Chrome-related parts

In controller.js is where we hack.

YSlow events

In YSlow 2.0 we decided to make use of a simple observer pattern implementation and fire events whenever interesting stuff happens. Especially useful since figuring all the data for all resources better be asynchronous.

Once yslow "peels" the page figuring out the components, gets each component data, headers, etc, then runs the lint rules, finally it fires a lintResultsReady event.

("Peeling a page" I heard for the first time from Steve Souders and for what I know he should be credited with this term describing the activity of figuring all components that go into a page)

All we need to do is listen to this event and send the beacon.

YSLOW.util.event.addListener(
  'lintResultReady', 
  function (o) {
    //...b-b-beacon! ...
  }
);

There's a YSLOW.util.sendBeacon() which does precisely that, so we need to call it and we're done.

Preferences

Firefox has a built-in (native) system to manage preferences. You know, the stuff you tweak in about:config. This is where we put the preferences - beacon yes/no, beacon URL, beacon data verbosity.

In Chrome such native preference system probably exists, but YSlow is currently not taking advantage. (Just guessing here.)

Luckily all calls to get preferences are abstracted in YSLOW.util.Preferences.getPref(prefname, defaultvalue). The default value is returned if a better one cannot be found.

So we can just overwrite the getPref() method to return the default value, unless it's a preference we care about, such as the beacon URL:

YSLOW.util.Preference.getPref = function(what, defaultval) {
  switch (what) {
    case 'beaconUrl':
      return 'http://www.phpied.com/beacon.png';
    case 'beaconInfo':
      return 'all'; // or "basic"
    default:
      return defaultval;
  }
};

Integration

As mentioned we'll hack into the controller.js, we don't want to touch the yslow core stuff. The controller.js is just one immediate function and our hack goes right before the last line. (Or even after it, probably doesn't matter)

(function () {
    // ... yslowy stuff ...
 
    // hack start
    // ...
    // hack end
 
    doc.ysview.setSplashView(...
}());

The complete thing is something like:

(function () {
 
    // ... slo, slo ...
 
    // hack start
    YSLOW.util.Preference.getPref = function(what, defaultval) {
      switch (what) {
        case 'beaconUrl':
          return 'http://www.phpied.com/beacon.png';
        case 'beaconInfo':
          return 'all'; // or "basic"
        default:
          return defaultval;
      }
    };
 
    YSLOW.util.event.addListener('lintResultReady', function (o) {
      var con = o.yslowContext,
          result = con.result_set;
      YSLOW.util.sendBeacon(result.url, result.overall_score, con);
    });
    // hack end
 
    doc.ysview.setSplashView(...
}());

Conclusion

So there - you can run YSlow in Chrome and send yourself (or showslow.com) beacons.

You probably don't need that so bad that you can't wait till next YSlow for Chrome ships with this thing working. But here it is.

And hopefully you learned a bit about YSlow internals so that you can start hacking yourself and/or wait till YSlow shows up on github (soon!) and start sending diffs. I personally can't wait.

Shoutout goes out to Marcel and Betty who are doing awesome stuff with YSlow (slides). And looks like even more is to come!

 

When is a stylesheet really loaded?

Thursday, March 17th, 2011

Often we want to load a CSS file on-demand by inserting a link node. And we want to know when the file finished loading in order to call a callback function for example.

Long story short: turns out this is harder than it should be and really unnecessary hard in Firefox. I hereby beg on behalf of many frustrated developers: please, Firefox 4, please fire a load event when a stylesheet loads.

Update:
If you think this is "a change we can believe in", tell the browser vendors to fire a load event on link elements as the HTML5 standard mandates. Here's the list of bugs for each browser to comment on and point people to:

Here's what the standard says:

"Once the attempts to obtain the resource and its critical subresources are complete, the user agent must, if the loads were successful, queue a task to fire a simple event named load at the link element"

With that out of the way, let's see what we have here.

// my callback function
// which relies on CSS being loaded
function CSSDone() {
  alert('zOMG, CSS is done');
}
  
 
// load me some stylesheet
var url = "http://tools.w3clubs.com/pagr/1.sleep-1.css",
    head = document.getElementsByTagName('head')[0];
    link = document.createElement('link');
 
link.type = "text/css";
link.rel = "stylesheet"
link.href = url;
 
// MAGIC
// call CSSDone() when CSS arrives
 
head.appendChild(link);

Options for the magic part, sorted from nice-and-easy to ridiculous

  1. listen to link.onload
  2. listen to link.addEventListener('load')
  3. listen to link.onreadystatechange
  4. setTimeout and check for changes in document.styleSheets
  5. setTimeout and check for changes in the styling of a specific element you create but style with the new CSS

5th option is too crazy and assumes you have control over the content of the CSS, so forget it. Plus it checks for current styles in a timeout meaning it will flush the reflow queue and can be potentially slow. The slower the CSS to arrive, the more reflows. So, really, forget it.

So how about implementing the magic?

  // MAGIC
  // #1
  link.onload = function () {
    CSSDone('onload listener');
  }
  // #2
  if (link.addEventListener) {
    link.addEventListener('load', function() {
      CSSDone("DOM's load event");
    }, false);
  }
  // #3
  link.onreadystatechange = function() {
    var state = link.readyState;
    if (state === 'loaded' || state === 'complete') {
      link.onreadystatechange = null;
      CSSDone("onreadystatechange");
    }
  };
  
  // #4
  var cssnum = document.styleSheets.length;
  var ti = setInterval(function() {
    if (document.styleSheets.length > cssnum) {
      // needs more work when you load a bunch of CSS files quickly
      // e.g. loop from cssnum to the new length, looking
      // for the document.styleSheets[n].href === url
      // ...
      
      // FF changes the length prematurely :( )
      CSSDone('listening to styleSheets.length change');
      clearInterval(ti);
      
    }
  }, 10);
  
  // MAGIC ends

Test

Test page - riiiight here. I'm loading a CSS file delayed two seconds on the server. Attaching all those event listeners and timeouts above. Adding another timeout that just says "... and two seconds later ... " after (you guessed it!) two seconds. Now observing los resultados...

Results

  • IE fires readystatechange and load (tested years ago, too lazy to test now again). Now with IE9 maybe addEventListener will work too?
  • Firefox (like before) fires nothing. It updates the length of document.styleSheets immediately, not waiting for the file to actually arrive. So the outcome in my test log is:
    zOMG, CSS #1 is done: listening to styleSheets.length change
    ... and two seconds later ...
    
  • Opera fires load via onload and via addEventListener too. Like FF it also increments document.styleSheets.length immédiatement. The outcome:
    zOMG, CSS #1 is done: listening to styleSheets.length change
    ... and two seconds later ...
    zOMG, CSS #1 is done: onload listener
    zOMG, CSS #1 is done: DOM's load event
  • Chrome and Safari will not fire events but will update document.styleSheets only when the file arrives, yey!
    ... and two seconds later ...
    zOMG, CSS #1 is done: listening to styleSheets.length change

All in all, there's at least one way to tell when the stylesheet is loaded in each browser, except Firefox. Now that's embarrassing.

Is there really no hope for Firefox?

If you go really crazy than yes - implement magic #5 but it has serious drawbacks.

Otherwise the object trick should do - all browsers seem to fire load and/or readystatechange event consistently. Obviously it's a little more complicated. Although probably not as complicated as monitoring the document.styleSheets collection

I also tried MozAfterPaint - it might work but didn't for me, because my CSS didn't change anything on the page that required repaint. Obviously not a fit-all solution.

Another thing that failed was checking document.styleSheets[n].cssRules. Although document.styleSheets.length is updated immediately, I was thinking FF cannot update document.styleSheets[n].cssRules (document.styleSheets[n].sheet.cssRules) until the CSS actually arrives. However since FF 3.5 (or thereabouts) you don't have access to cssRules collection when the file is hosted on a different domain (CDN say). Security thing, you see. Even cssRules.length would've been enough, but nah.

Takeaways

  1. Any ideas about a clever (or not so clever) workaround that lets us figure out when CSS is loaded in FF? Please comment.
  2. Libraries can benefit from document.styleSheets.length trick to support Chrome and Safari. I know at least that YUI3 doesn't support callbacks on Y.Get.css() in Safari (nor FF)
  3. Firefox 4 must implement load on stylesheets :) No doubt about it. IMO all browsers should fire load on everything related to external resources.

UPDATE: FF solved!

Thanks to Ryan's comment below, Zach and Oleg, turns out there is something that works. And that is:

  1. you create a style element, not a link
  2. add @import "URL"
  3. poll for access to that style node's cssRules collection

It just happens so that Firefox will not populate this collection until the file arrives!

var style = document.createElement('style');
style.textContent = '@import "' + url + '"';
 
var fi = setInterval(function() {
  try {
    style.sheet.cssRules; // <--- MAGIC: only populated when file is loaded
    CSSDone('listening to @import-ed cssRules');
    clearInterval(fi);
  } catch (e){}
}, 10);  
 
head.appendChild(style);

Updated my test and seems to work just fine.

Whew!

I still maintain that this is unnecessarily complex and all browsers should simply fire load event. Seems to me this FF behavior might change at any time, as well as Safari's not populating document.styleSheet

One thing to note: this access to cssRules is not a failure of the same security check that doesn't allow cssRules access with outside domains. In this case we're accessing cssRules of the inline style and that's fine. We still cant access the styles of the @import-ed file from a different domain.

Side note: makes me wonder - this could be a technique to preload JS without executing too :)

 

Velocity online conference March 2011

Tuesday, March 15th, 2011

Very rough notes from the event that just ended a few minutes ago

======== 1 ========

Tony Gentilcore (google) and Anderson Quach (MSFT) talked about the W3C working group and the specs for measuring performance:

  • Navigation timing spec - HTML document: DNS, handshakes, load events
  • Resource timing - how long JS, CSS, audio, video... take
  • User timing - interactions, such as clicking a link

Challenges: user security, privacy. Small overhead for browsers. Useful actionable data out.

Navigation timing:

  • window.performance - IE9 has no vendor prefix
  • otherwise window.webkitPerformance
  • FF4 probably shipping it too

Properties:

window.performance.timing
  .domainLookup
  .connect
  .domContentLoaded
  .loadEvent
  ....etc

Specs: http://test.w3.org/webperf/specs/NavigationTiming/

Resource Timing spec - upcoming

foreach resource 
  redirect
  fetchStart
  fetchEnd
  ...

User Timing - upcoming

  mark('pagelet')
  mark('something-else')
  ...
  
  >>> getMarks();
  {
    'pagelet': 123456767
  }

Stretch goals:

  • painting times
  • CPUs
  • visibility (is the page in a background tab)
  • allow real yealding setTimeout(0)
  • don't animate faster than the browser paints

mailto: public-web-perf@w3c.org

======== 2 ========

Above the Fold Time: Measuring Web Page Performance Visually
Speaker: Jake Brutlag
Help from: Zoe Abrams, Pat Meenan (webpagetest) Google

What the user perception of page load timing is.

AFT (Above the Fold Timing) algo
AFT: When content above the fold stops changing and reaches its final state

Integrated in WebPageTest.org

Motivation

  • AFT more representative than DOMContentLoaded or onload
  • onload - different in IE and FF, e.g. async scripts block onload or not

Method:

  • video capture
  • AFT cutoff time - upper bound, e.g. 45 sec for DSL
  • split pixels into static and dymanic (ads)
  • AFT = latest change of a static pixel

Magic number - 5 determined empirically
static pixels are those that change less than 5 times

Application - validate other metrics, cross-browser, visual
Limitation - lab only

{I ran webpagetest.org on facebook.com with AFT on.
The results are very close to onload, as expected.
It's a simple page. Results:

http://www.webpagetest.org/result/110315_P9_5MDJ/

}

== break ==

Note from the sponsor StrangeLoop: a downloadable "State of the Union"
A study of top 2000 Alexa retail sites. Findings:

  1. 11.2 sec onload avg
  2. top 100 sites slower than the lower ranked
  3. those using CDNs are not faster
  4. failed to implement the basics - YSlow stuff

======== 3 ========

Next-gen YSlow
Betty Tso and Marcel Duran, Yahoo!

YSlow for Chrome last week
Not as accurate as Firefox cause headers, timing come from:
1. Crawl DOM
2. x-domain requests

Announcing YSlow bookmarklet version
for mobile and all other browsers to be released early april
YSlow now on Git?

Lots coming up, e.g:

  • Multi-language support
  • HAR-to-YSlow
  • YSlow export to JDrop

Bookmarket works like:
1. crawl DOM
2. Use YQL to get headers

     SELECT * FROM data.headers WHERE
      url IN ('http://...', 'http://...')

======== 4 ========

Visibility into Mobile Performance
Steve Souders, Google

Demo #1: http://blaze.io/mobile/
WebPageTest.org framework connected to iPhone and Android apps
Mobile waterfalls
Demo #2: pcapperf
1. setup wifi hotspot on a pc/mac
2. tcpdump to capture .pcap file
3. upload .pcap to http://pcapperf.appspot.com app
4. observe waterfall via pcap2har and harviewer
5. run pagespeed on the har
Demo #3: JDrop
http://jdrop.org
Upload JSON so you can run something on mobile and view/analyze results in desktop's large screen
Works with the bookmarklet tools part of Steve's package of mobile meta-tool

 

Automating HTTPWatch with PHP #3

Monday, March 7th, 2011

The first part is here, the second is here. This third post is more about PHP and COM, rather than HTTPWatch or monitoring web performance, so feel free to skip if the title mislead you :) Keep reading if you want to use and improve/update my HTTPWatch class in the future.

The problem

After running a HTTPWatch-ed browser via a script I wanted to have an easy way to dump all the data collected. Since the PHP-HTTPWatch bridge is via COM interface, all the objects returned by HTTPWatch are Variants and not ready for introspection with the usual PHP functions, like get_object_vars() for example.

The solution

Interestingly, turns out there exist a function called com_print_typeinfo(). You give it a COM object (works with those variants HTTPWatch gives you) and it returns you source code for a PHP class defining this COM object. So the hack here is to evaluate this source code with eval() (oh, the horror!) and then inspect it with get_class_vars(). Luckily there's no naming collision between the PHP built-in classes and those defied by HTTPWatch.

// the input is a class name 
// and an object of that class
$class = "Entry";
$object = $http->watch->Log->Entries->Item(0);
 
// buffer output
ob_start();
// print out class definition
// derived from an object
com_print_typeinfo($obj);
$typeinfo = ob_get_contents();
ob_end_clean();
 
// evaluate the generated PHP source
eval($typeinfo);
 
// get the properties
$properties = get_class_vars($class);
 
// Horay!

In order for this to work, as you can see, we need to know the class names and we need access to an example object of each class. This is where I needed to study the HTTPWatch API and find a suitable example page that will generate enough objects to derive the API from.

The free HTTPWatch version

The free version has restrictions where you don't have access to all properties. I wanted my class to be able to do the best job possible with or without the presence of restrictions. That's why I first load google.com which is unrestricted and then an image on my blog, which is restricted.

From the first page I derive the complete API (that I'm interested in) and then I use the derived API to study the second URL request. Accessing each property in a try-catch blows up when a property is restricted, so I write it to a second array of API properties $paidproperties.

Source: the end

In the end when you run the script dumpapi.php. It uses my HTTPWatch class to derive the API for the HTPWatch class itself. How meta! The result of the run you write to an API file and then this file is included by the class. Nice and clean after a messy hack :)

Run:

$ php dumpapi.php > HTTPWatchAPI.php

(This is a one-off operation, no need to run it at all if you don't need to change anything in the class)

Then, before you instantiate the constructor, you go:

// point to the API dump
HTTPWatch::$apipath = "HTTPWatchAPI.php";
 
// the usual
$http = new HTTPWatch();

The default name and location for that API dump is the same directory and file name HTTPWatchAPI.php, so you skip that first line unless you have a valid reason to store the API in a different location.

 

Automating HTTPWatch with PHP #2

Monday, March 7th, 2011

In part 1 I demonstrated how you can use PHP to script and automate HTTPWatch. And how you can get data back, either reading the API docs or using a quick HAR hack to get a lot of data in one go.

Now I want to share a little class I wrote to make all that a little easier.

The code is here on GitHub.

Basic usage

Open IE, navigate, close:

$http = new HTTPWatch();
$http->go('http://phpied.com/');
$http->done();

To do the same in Firefox, just pass "ff" to the constructor:

$http = new HTTPWatch('ff');
$http->go('http://phpied.com/');
$http->done();

The constructor accepts a second param with options, like empty cache, hide browser (ie only), etc, largely underused for the time being.

Handle to the HTTPWatch plugin

After $http = new HTTPWatch(); a watch property will be added to $http. This is the HTTPWatch instance which gives you access to all its APIs, so you can do e.g.

$summary = $http->watch->Log->Entries->Summary;

Data out

My main motivation behind this class, other than simpler api, has been to provide the ability to just dump all the data that HTTPWatch collects in a quick print_r(). That has been a challenge with the COM PHP bridge, but I found a hack around it. In any event, most of the HTTPWatch API I've exported to a second PHP file - the HTTPWatchAPI.php script. (This is an auto-generated file, created by another script, but let's leave that out for now.)

So after you've navigated to a page you have two convenient methods to grab a bunch of data from HTTPWatch. The first is:

$http->getSummary();

This gives a summary stats for the http observation session. The second is

$http->getEntries();

It gives you details about every HTTPWatch log entry - be it cached or an actual HTTP request.

Here's an example of what getSummary() can give you. Here's how this file was generated:

$http = new HTTPWatch();
$http->go("http://google.com");
print_r($http->getSummary());
$http->done();

And here's some output print_r()-ed from getEntries(). Here's the code that produced it:

$http = new HTTPWatch();
$http->go("http://google.com");
print_r($http->getEntries());
$http->done();

If you look carefully at the dump, you may notice something like [Stream] => [BYTESTREAM]. Most of the times you don't need the raw HTTP streams (gzipped, chunked, etc), but you can get them if you want by setting:

$http->skipStreams = false;

Here's the same google.com example, this time including the raw streams. And the code:

$http = new HTTPWatch();
$http->go("http://google.com");
$http->skipStreams = false;
print_r($http->getEntries());
$http->done();

Free vs. paid

One pain with HTTPWatch is that the free version has restrictions. The summary for example doesn't include TimingSummaries and WarningSummaries properties. The entry log has almost nothing - no headers or content or streams. My class handles that by giving you as much as it can. If you're using the free version, it will return the limited data for the restricted URLs, but still the full data for those URLs that HTTPWatch's demo version allows - the top Alexa sites.

So here's a dump of visiting http://givepngachance.com with my free HTTPWatch edition.

The data has restricted information about givepngachance.com URL but full data related to the embeded youtube.com resource.

The code:

$http = new HTTPWatch();
$http->go("http://givepngachance.com");
print_r($http->getEntries());
$http->done();

Again with the video

If you've read part 1, you've probably seen the video, but here's the link again (try the HD version). This is a screencapture of loading FF and IE using my new class. The code that produced it is:

$ie = new HTTPWatch();
$ie->go('http://google.com/');
$sum = $ie->getSummary();
$ff = new HTTPWatch('ff');
$ff->go('http://google.com/');
$sumff = $ff->getSummary();
 
echo "\nRun 1 ";
echo $ie->watch->Log->BrowserName, ' ';
echo $ie->watch->Log->BrowserVersion;
echo "\nSent: ", $sum['BytesSent'], "; Received: ", $sum['BytesReceived'];
 
echo "\nRun 2 ";
echo $ff->watch->Log->BrowserName, ' ';
echo $ff->watch->Log->BrowserVersion;
echo "\nSent: ", $sumff['BytesSent'], "; Received: ", $sumff['BytesReceived'];
 
$ie->done();
$ff->done();

As you can see I'm accessing the HTTPWatch plugin object ($http->watch) directly to get the browser version and name. I didn't think this was worth wrapping in a more convenient API the way I did with getSummary() and getEntries().

The result of this is:

$ php examples.php

Run 1 Internet Explorer 6.0.2900.5512
Sent: 7102; Received: 89188
Run 2 Firefox 3.5.6
Sent: 6388; Received: 166473 

If you're wondering why FF gets twice the bytes, it's because google.com in IE6 is very basic - no search-as-you-type so much less JavaScript and one less sprite.

That's all folks

Enjoy, fork and keep an eye on what's up with the HTTP traffic. What goes through the tubes is too important not to be observed and monitored :)

 

Automating HTTPWatch with PHP

Saturday, March 5th, 2011

HTTPWatch is a nice tool to inspect HTTP traffic in easy and convenient way and it works in both IE and FF now. Drawback - windows-only and paid. But the free version is good enough for many tasks.

HTTPWatch can be automated and scripted which is pretty cool for a number of monitoring-like tasks. Their site and help section lists C# and Ruby+Watir examples. So I was curious - what about PHP (and no Watir).

In general with PHP you can open/close/navigate IE using COM (whatever that is) which is nice, but you can't do that with Firefox as it doesn't expose a COM interface. But HTTPWatch fills the gap. K, let's see an example.

Prerequisites

OS: Windows
Install: IE, Firefox, HTTPWatch, php (command-line is fine, no need for Apache, MySQL, etc)

Getting started

Create a file, say C:\http.php, open command prompt and go:

cd \
C:\>php http.php

Now all that's left is to put something worth executing in http.php :)

Instantiating HTTPWatch

$controller = new COM("HttpWatch.Controller");
if(!method_exists($controller, 'IE')) {
  throw new Exception('failed to enable HTTPWatch');
}

Opening a new Firefox window:

$plugin = $controller->Firefox->New();

BTW, it's the same for IE:

$plugin = $controller->IE->New();

Disabling any filters (filters defined in HTTPWatch that is)

$plugin->Log->EnableFilter(false);

Clear HTTPWatch's log (the list of requests), clear the browser cache and start recording traffic:

// clear log and cache
$plugin->Clear();
$plugin->ClearCache();
 
// start
$plugin->Record();

Navigate to a URL and wait for it to complete - that means wait a bit after onload even

// browse
$plugin->GotoUrl('http://google.com');
$controller->Wait($plugin, -1);

Stop monitoring traffic and quit the browser:

$plugin->Stop();
$plugin->CloseBrowser();

This is nice, we opened the browser, visited a URL and closed. Now we can even get some meaningful data out of the whole experience.

$plugin->Log->Entries is an object that has a list of all requests. It also has a property Summary. So we can see how many bytes we sent and how many received as a result of this visit to google.com

$sum = $plugin->Log->Entries->Summary;
echo "in: {$sum->BytesReceived}, out: {$sum->BytesSent}";

Note: oh, you need to get your data before closing the browser, otherwise the Log object gets destroyed it seems

So the result:

C:\>php http.php
in: 89185, out: 7102

Yeah!

This may look like nothing, but is pretty impressive in an of itself. At least I know I was happy the first time it worked. Because, you see, any monitoring that doesn't use a real browser is kinda smelly, isn't that right? Plus this is awesome for performance tests, research and experiments. You can create page A and page B and go out for a walk. Meanwhile your script can load the pages 200 times in the two browsers (at least, because you can have FF+IE[678]), with empty and full cache... and you come back for the results! Tired of all the walking, not of hitting REFRESH.

Below you can see (HD!) video of a script that opens IE and FF, loads Google and then gives you the bytes in/out in the two browsers. This example uses a PHP class I created and will talk about later, but you can still see the idea.

A better experience in IE

One thing I don't like is that HTTPWatch won't let you control the browser very well. Two features I'm looking for: being able to see HTTPWatch's log while running (for testing) and then being able to completely hide the window (for "production"). Luckily IE let's you do that and HTTPWatch let's you "attach" an already running IE instance.

So. We open IE with its own COM interface:

$browser = new COM("InternetExplorer.Application");
if(!method_exists($browser, 'Navigate')) {
  throw new Exception('didn\'t create IE obj');
}
$browser->Visible = true;

As you can see - not very different. But there's Visibile which can be false if you so like. This way you can still work on something while tests are running in the background without windows popping up all the time.

Also if you open HTTPWatch manually and close the browser, then the next time (in your scripted runs) HTTPWatch will stay open and you can check what's up.

So, connecting HTTPWatch with the IE instance means instantiating HTTPWatch as before and passing the IE object to Attach() method (was New() before).

// watch this!
$controller = new COM("HttpWatch.Controller");
if(!method_exists($controller, 'IE')) {
  throw new Exception('failed to enable HTTPWatch');
}
 
// enable plugin
$plugin = $controller->IE->Attach($browser);

The rest is all the same.

There's more

The most interesting part is getting data back from HTTPWatch. Dunno about you, but I love just dumping whatever structure I have with print_r() or var_dump() and then deciding what I want from it and how to to go about getting it.

That doesn't happen here because these COM objects are Variants and you can't just dump'em. You have to read the API docs. That sucks. So I did a hack (next post) and also read the APIs ("Stoyan: reading the APIs so you don't have to!") to enable just dumping the httpwatch's log.

Meawhile...

HAR

HTTPWatch can write you a HAR file with the log. Not everything is in there, but it's still a lot and it's easy. HAR is JSON so you json_decode() it and voila - a log!

$filename = tempnam('/tmp', 'watchmenowimgoindown');
$plugin->Log->ExportHAR($filename);
$json = file_get_contents($filename);
 
print_r(json_decode($json));

If you're curious as to what that prints - here it is.

Want to see a HAR (from another run)? Here it is.

So here you go - much data can be extracted and dumped for inspection from the HAR output. For the full httpwatch data, there's the API.

(to be continued...)

 

CSS minifiers comparison

Saturday, October 30th, 2010

Last year I compared some CSS minifiers, namely YUICompressor, CSSTidy (with "small" vs. "safe" settings), PHP PEAR's CSS lib and Minify (detailed results). Now that I've done some work on the YUICompressor and since there's a new kid on the block from Microsoft I thought I should give it another go.

I only compared CSSTidy with best compression, YUICompressor and MS' Ajaxmin. The verdict is still the same - CSSTidy looks the best, but there really isn't that big of a difference between the three (and virtually no difference between YUIC and Ajaxmin), especially if you take into account gzipping.

Still on average you get about 35% size reduction when you minify and 80% when you gzip the minified CSS. Meaning when all is done your users only download 19-20% of what they would normally do if you don't perform these simple optimizations.

Experiment

Source CSS files came from csszengarden.com - 213 CSS files.

For the YUICompressor I used the JS verison, on windows with WSH, described in the previous post.

For CSSTidy I used exactly the same code as the last year.

Lastly, Ajaxmin I ran with the default options:

$ AjaxMin.exe in.css -out out.css

Results

CSSTidy wins with bringing the size down to 63.29% of the original on avegare. The other two follow closely with 66.47% (ajaxmin) and 66.41% (YUI). After gzipping: 19.44% (tidy), 19.62% (ajaxmin) and 19.63% (yui). In other words YUI's cssmin performs ever so slightly better that Ajaxmin by 0.06% before gzipping, but after gzipping Ajaxmin wins with 0.01%.

Overall the results show that you shouldn't sweat too much over the choice of CSS minifier. They are all pretty close and you should pick the one that is most convenient for your own use and build process.

CSSTidy and Ajaxmin have parsers, so theoretically they have a much better chance. YUI's cssmin is a bunch of regular expressions and no proper parsing. In reality they are all performing really similarly. Part of the reason is that CSS is not as minifiable as JavaScript as most of it is long selectors and long prperty names (background-image, text-decoration, -webkit-transform-origin, anyone?), which are not renamed to shorter names the way JS variables are.

One random observation (and hint for Ajaxmin folks) is that cssmin strips selectors with empty rulesets while Ajaxmin does not. E.g. #selector {} is totally gone in cssmin but stays in ajaxmin.

Detailed results

File Original cssminjs CSSTidy-small ajaxmin
001.css
gzipped
4832b 100.00%
1781b   36.86%
2650b   54.84%
888b   18.38%
2586b   53.52%
902b   18.67%
2653b   54.90%
892b   18.46%
002.css
gzipped
5242b 100.00%
1936b   36.93%
2651b   50.57%
893b   17.04%
2605b   49.69%
898b   17.13%
2651b   50.57%
893b   17.04%
003.css
gzipped
4964b 100.00%
1856b   37.39%
2734b   55.08%
996b   20.06%
2614b   52.66%
979b   19.72%
2733b   55.06%
992b   19.98%
004.css
gzipped
3854b 100.00%
1285b   33.34%
2370b   61.49%
748b   19.41%
2335b   60.59%
752b   19.51%
2370b   61.49%
748b   19.41%
005.css
gzipped
4648b 100.00%
1616b   34.77%
2652b   57.06%
882b   18.98%
2604b   56.02%
882b   18.98%
2655b   57.12%
884b   19.02%
006.css
gzipped
4319b 100.00%
1435b   33.23%
2689b   62.26%
849b   19.66%
2614b   60.52%
863b   19.98%
2688b   62.24%
850b   19.68%
007.css
gzipped
5412b 100.00%
1557b   28.77%
3694b   68.26%
997b   18.42%
3459b   63.91%
990b   18.29%
3690b   68.18%
993b   18.35%
008.css
gzipped
2143b 100.00%
961b   44.84%
1260b   58.80%
557b   25.99%
1194b   55.72%
553b   25.80%
1260b   58.80%
557b   25.99%
009.css
gzipped
5525b 100.00%
1515b   27.42%
3490b   63.17%
960b   17.38%
3237b   58.59%
946b   17.12%
3490b   63.17%
960b   17.38%
010.css
gzipped
4148b 100.00%
1474b   35.54%
2301b   55.47%
868b   20.93%
2172b   52.36%
853b   20.56%
2307b   55.62%
867b   20.90%
011.css
gzipped
6021b 100.00%
1612b   26.77%
3540b   58.79%
1029b   17.09%
3425b   56.88%
1018b   16.91%
3536b   58.73%
1030b   17.11%
012.css
gzipped
9250b 100.00%
3113b   33.65%
4914b   53.12%
1421b   15.36%
4882b   52.78%
1410b   15.24%
4925b   53.24%
1426b   15.42%
013.css
gzipped
5846b 100.00%
1811b   30.98%
3731b   63.82%
1030b   17.62%
3643b   62.32%
1013b   17.33%
3734b   63.87%
1031b   17.64%
014.css
gzipped
6010b 100.00%
1518b   25.26%
4355b   72.46%
1063b   17.69%
4332b   72.08%
1083b   18.02%
4358b   72.51%
1064b   17.70%
015.css
gzipped
6337b 100.00%
1875b   29.59%
3918b   61.83%
1109b   17.50%
3562b   56.21%
1081b   17.06%
3936b   62.11%
1113b   17.56%
016.css
gzipped
6934b 100.00%
1629b   23.49%
4978b   71.79%
1179b   17.00%
4168b   60.11%
1175b   16.95%
4982b   71.85%
1181b   17.03%
017.css
gzipped
4964b 100.00%
1626b   32.76%
3266b   65.79%
1101b   22.18%
3245b   65.37%
1110b   22.36%
3266b   65.79%
1101b   22.18%
018.css
gzipped
4000b 100.00%
1424b   35.60%
2521b   63.03%
859b   21.48%
2467b   61.68%
862b   21.55%
2519b   62.98%
856b   21.40%
019.css
gzipped
5265b 100.00%
1469b   27.90%
3376b   64.12%
869b   16.51%
3268b   62.07%
879b   16.70%
3406b   64.69%
886b   16.83%
020.css
gzipped
7262b 100.00%
1844b   25.39%
5668b   78.05%
1416b   19.50%
5654b   77.86%
1393b   19.18%
5668b   78.05%
1414b   19.47%
021.css
gzipped
7118b 100.00%
1849b   25.98%
5602b   78.70%
1350b   18.97%
5129b   72.06%
1347b   18.92%
5602b   78.70%
1350b   18.97%
022.css
gzipped
7235b 100.00%
1888b   26.10%
5065b   70.01%
1172b   16.20%
4789b   66.19%
1161b   16.05%
5065b   70.01%
1172b   16.20%
023.css
gzipped
5192b 100.00%
1558b   30.01%
3648b   70.26%
1070b   20.61%
3613b   69.59%
1074b   20.69%
3648b   70.26%
1065b   20.51%
024.css
gzipped
4537b 100.00%
1434b   31.61%
3056b   67.36%
950b   20.94%
2901b   63.94%
936b   20.63%
3062b   67.49%
951b   20.96%
025.css
gzipped
10006b 100.00%
2376b   23.75%
7025b   70.21%
1614b   16.13%
6685b   66.81%
1595b   15.94%
7031b   70.27%
1615b   16.14%
026.css
gzipped
4246b 100.00%
1256b   29.58%
2863b   67.43%
829b   19.52%
2799b   65.92%
826b   19.45%
2863b   67.43%
829b   19.52%
027.css
gzipped
5624b 100.00%
2298b   40.86%
2651b   47.14%
942b   16.75%
2559b   45.50%
941b   16.73%
2651b   47.14%
942b   16.75%
028.css
gzipped
5913b 100.00%
1595b   26.97%
3557b   60.16%
932b   15.76%
2829b   47.84%
849b   14.36%
3563b   60.26%
932b   15.76%
029.css
gzipped
5216b 100.00%
1591b   30.50%
3980b   76.30%
1098b   21.05%
3637b   69.73%
1060b   20.32%
3980b   76.30%
1098b   21.05%
030.css
gzipped
3810b 100.00%
1350b   35.43%
2425b   63.65%
838b   21.99%
2455b   64.44%
839b   22.02%
2423b   63.60%
837b   21.97%
031.css
gzipped
4448b 100.00%
1423b   31.99%
2556b   57.46%
911b   20.48%
2557b   57.49%
905b   20.35%
2556b   57.46%
908b   20.41%
032.css
gzipped
5243b 100.00%
1474b   28.11%
3991b   76.12%
985b   18.79%
3813b   72.73%
992b   18.92%
3984b   75.99%
980b   18.69%
033.css
gzipped
6091b 100.00%
1817b   29.83%
4442b   72.93%
1302b   21.38%
4208b   69.09%
1263b   20.74%
4498b   73.85%
1314b   21.57%
034.css
gzipped
6319b 100.00%
1555b   24.61%
4913b   77.75%
1090b   17.25%
4879b   77.21%
1092b   17.28%
4916b   77.80%
1090b   17.25%
035.css
gzipped
10877b 100.00%
2320b   21.33%
6718b   61.76%
1595b   14.66%
6192b   56.93%
1508b   13.86%
6718b   61.76%
1595b   14.66%
036.css
gzipped
7117b 100.00%
2122b   29.82%
4229b   59.42%
1181b   16.59%
4035b   56.70%
1153b   16.20%
4229b   59.42%
1181b   16.59%
037.css
gzipped
7738b 100.00%
2210b   28.56%
4624b   59.76%
1166b   15.07%
4417b   57.08%
1168b   15.09%
4627b   59.80%
1166b   15.07%
038.css
gzipped
3459b 100.00%
1147b   33.16%
2173b   62.82%
680b   19.66%
2181b   63.05%
681b   19.69%
2173b   62.82%
676b   19.54%
039.css
gzipped
6885b 100.00%
2129b   30.92%
3821b   55.50%
1115b   16.19%
3595b   52.21%
1093b   15.88%
3836b   55.72%
1117b   16.22%
040.css
gzipped
6772b 100.00%
1699b   25.09%
4580b   67.63%
1132b   16.72%
4542b   67.07%
1119b   16.52%
4580b   67.63%
1132b   16.72%
041.css
gzipped
4398b 100.00%
1803b   41.00%
2000b   45.48%
764b   17.37%
1973b   44.86%
754b   17.14%
2002b   45.52%
762b   17.33%
042.css
gzipped
14460b 100.00%
2568b   17.76%
5211b   36.04%
1466b   10.14%
4916b   34.00%
1449b   10.02%
5245b   36.27%
1471b   10.17%
043.css
gzipped
10762b 100.00%
2326b   21.61%
6929b   64.38%
1502b   13.96%
6241b   57.99%
1484b   13.79%
6929b   64.38%
1502b   13.96%
044.css
gzipped
7479b 100.00%
2112b   28.24%
4953b   66.23%
1311b   17.53%
4696b   62.79%
1286b   17.19%
4916b   65.73%
1293b   17.29%
045.css
gzipped
4470b 100.00%
1310b   29.31%
2930b   65.55%
847b   18.95%
2680b   59.96%
841b   18.81%
2933b   65.62%
848b   18.97%
046.css
gzipped
4416b 100.00%
1575b   35.67%
2480b   56.16%
822b   18.61%
2353b   53.28%
817b   18.50%
2480b   56.16%
822b   18.61%
047.css
gzipped
3467b 100.00%
1354b   39.05%
1992b   57.46%
796b   22.96%
2015b   58.12%
799b   23.05%
1992b   57.46%
796b   22.96%
048.css
gzipped
4600b 100.00%
1512b   32.87%
3339b   72.59%
1077b   23.41%
3308b   71.91%
1086b   23.61%
3344b   72.70%
1079b   23.46%
049.css
gzipped
6217b 100.00%
1742b   28.02%
3898b   62.70%
977b   15.71%
3833b   61.65%
977b   15.71%
3901b   62.75%
978b   15.73%
050.css
gzipped
8504b 100.00%
2056b   24.18%
6800b   79.96%
1535b   18.05%
5865b   68.97%
1481b   17.42%
6803b   80.00%
1536b   18.06%
051.css
gzipped
8915b 100.00%
2047b   22.96%
5783b   64.87%
1141b   12.80%
4829b   54.17%
1106b   12.41%
5837b   65.47%
1148b   12.88%
052.css
gzipped
9312b 100.00%
2319b   24.90%
6234b   66.95%
1461b   15.69%
5835b   62.66%
1451b   15.58%
6232b   66.92%
1462b   15.70%
053.css
gzipped
5882b 100.00%
1563b   26.57%
4416b   75.08%
1056b   17.95%
4242b   72.12%
1059b   18.00%
4416b   75.08%
1056b   17.95%
054.css
gzipped
7709b 100.00%
1989b   25.80%
5604b   72.69%
1406b   18.24%
5259b   68.22%
1371b   17.78%
5604b   72.69%
1406b   18.24%
055.css
gzipped
4890b 100.00%
1486b   30.39%
2983b   61.00%
948b   19.39%
2755b   56.34%
919b   18.79%
2983b   61.00%
948b   19.39%
056.css
gzipped
4770b 100.00%
1185b   24.84%
3630b   76.10%
1034b   21.68%
3241b   67.95%
984b   20.63%
3636b   76.23%
1034b   21.68%
057.css
gzipped
12063b 100.00%
3047b   25.26%
10382b   86.06%
2483b   20.58%
9230b   76.51%
2178b   18.06%
9505b   78.79%
2213b   18.35%
058.css
gzipped
8721b 100.00%
1936b   22.20%
6070b   69.60%
1329b   15.24%
5102b   58.50%
1328b   15.23%
6073b   69.64%
1328b   15.23%
059.css
gzipped
6660b 100.00%
1934b   29.04%
4879b   73.26%
1296b   19.46%
4321b   64.88%
1259b   18.90%
4888b   73.39%
1296b   19.46%
060.css
gzipped
4691b 100.00%
1566b   33.38%
2823b   60.18%
922b   19.65%
2629b   56.04%
919b   19.59%
2843b   60.61%
933b   19.89%
061.css
gzipped
4299b 100.00%
1327b   30.87%
2813b   65.43%
850b   19.77%
2803b   65.20%
857b   19.93%
2813b   65.43%
850b   19.77%
062.css
gzipped
15125b 100.00%
2757b   18.23%
11437b   75.62%
2010b   13.29%
10754b   71.10%
1939b   12.82%
11506b   76.07%
2015b   13.32%
063.css
gzipped
6751b 100.00%
2116b   31.34%
4213b   62.41%
1261b   18.68%
4164b   61.68%
1247b   18.47%
4213b   62.41%
1261b   18.68%
064.css
gzipped
5630b 100.00%
1772b   31.47%
4038b   71.72%
1209b   21.47%
3846b   68.31%
1195b   21.23%
4041b   71.78%
1207b   21.44%
065.css
gzipped
5630b 100.00%
1713b   30.43%
4104b   72.90%
1218b   21.63%
3970b   70.52%
1209b   21.47%
4110b   73.00%
1217b   21.62%
066.css
gzipped
5142b 100.00%
1533b   29.81%
3418b   66.47%
938b   18.24%
3088b   60.05%
915b   17.79%
3418b   66.47%
938b   18.24%
067.css
gzipped
4859b 100.00%
1605b   33.03%
3439b   70.78%
1059b   21.79%
3291b   67.73%
1034b   21.28%
3439b   70.78%
1059b   21.79%
068.css
gzipped
9506b 100.00%
2188b   23.02%
4220b   44.39%
1215b   12.78%
4160b   43.76%
1194b   12.56%
4220b   44.39%
1209b   12.72%
069.css
gzipped
4682b 100.00%
1494b   31.91%
3374b   72.06%
1064b   22.73%
3419b   73.02%
1069b   22.83%
3374b   72.06%
1064b   22.73%
070.css
gzipped
5300b 100.00%
1561b   29.45%
3621b   68.32%
1042b   19.66%
3530b   66.60%
1039b   19.60%
3623b   68.36%
1042b   19.66%
071.css
gzipped
4198b 100.00%
1314b   31.30%
2898b   69.03%
897b   21.37%
2752b   65.56%
866b   20.63%
2898b   69.03%
897b   21.37%
072.css
gzipped
5029b 100.00%
1636b   32.53%
3666b   72.90%
1137b   22.61%
3399b   67.59%
1095b   21.77%
3664b   72.86%
1134b   22.55%
073.css
gzipped
3409b 100.00%
1207b   35.41%
2480b   72.75%
838b   24.58%
2417b   70.90%
844b   24.76%
2480b   72.75%
838b   24.58%
074.css
gzipped
5875b 100.00%
1383b   23.54%
3817b   64.97%
879b   14.96%
3347b   56.97%
848b   14.43%
3859b   65.69%
886b   15.08%
075.css
gzipped
6956b 100.00%
1986b   28.55%
3748b   53.88%
1086b   15.61%
3599b   51.74%
1091b   15.68%
3754b   53.97%
1087b   15.63%
076.css
gzipped
6064b 100.00%
1508b   24.87%
4295b   70.83%
1094b   18.04%
4260b   70.25%
1075b   17.73%
4295b   70.83%
1094b   18.04%
077.css
gzipped
6255b 100.00%
1553b   24.83%
4410b   70.50%
1077b   17.22%
3982b   63.66%
1061b   16.96%
4417b   70.62%
1074b   17.17%
078.css
gzipped
6209b 100.00%
1543b   24.85%
3960b   63.78%
945b   15.22%
3543b   57.06%
921b   14.83%
3960b   63.78%
945b   15.22%
079.css
gzipped
2119b 100.00%
1000b   47.19%
1380b   65.13%
627b   29.59%
1399b   66.02%
629b   29.68%
1381b   65.17%
629b   29.68%
080.css
gzipped
5033b 100.00%
1414b   28.09%
3548b   70.49%
957b   19.01%
3351b   66.58%
949b   18.86%
3560b   70.73%
961b   19.09%
081.css
gzipped
8661b 100.00%
1852b   21.38%
6091b   70.33%
1266b   14.62%
5216b   60.22%
1247b   14.40%
6109b   70.53%
1271b   14.67%
082.css
gzipped
5389b 100.00%
1763b   32.71%
3370b   62.53%
1089b   20.21%
3214b   59.64%
1072b   19.89%
3370b   62.53%
1088b   20.19%
083.css
gzipped
6045b 100.00%
1741b   28.80%
4200b   69.48%
1128b   18.66%
4110b   67.99%
1126b   18.63%
4199b   69.46%
1128b   18.66%
084.css
gzipped
4865b 100.00%
1593b   32.74%
3453b   70.98%
921b   18.93%
3273b   67.28%
923b   18.97%
3453b   70.98%
921b   18.93%
085.css
gzipped
4434b 100.00%
1433b   32.32%
3065b   69.12%
956b   21.56%
2843b   64.12%
928b   20.93%
3068b   69.19%
956b   21.56%
086.css
gzipped
7007b 100.00%
1727b   24.65%
2806b   40.05%
947b   13.52%
2792b   39.85%
945b   13.49%
2806b   40.05%
942b   13.44%
087.css
gzipped
7422b 100.00%
1839b   24.78%
3157b   42.54%
996b   13.42%
3158b   42.55%
986b   13.28%
3157b   42.54%
990b   13.34%
088.css
gzipped
13936b 100.00%
3351b   24.05%
7685b   55.14%
1671b   11.99%
6673b   47.88%
1623b   11.65%
7697b   55.23%
1675b   12.02%
089.css
gzipped
4766b 100.00%
1511b   31.70%
3609b   75.72%
1068b   22.41%
3513b   73.71%
1041b   21.84%
3609b   75.72%
1068b   22.41%
090.css
gzipped
5986b 100.00%
1792b   29.94%
4104b   68.56%
1246b   20.82%
3953b   66.04%
1217b   20.33%
4095b   68.41%
1238b   20.68%
091.css
gzipped
3738b 100.00%
1288b   34.46%
2446b   65.44%
902b   24.13%
2399b   64.18%
901b   24.10%
2449b   65.52%
896b   23.97%
092.css
gzipped
4246b 100.00%
1474b   34.72%
2770b   65.24%
997b   23.48%
2761b   65.03%
1001b   23.58%
2818b   66.37%
1001b   23.58%
093.css
gzipped
5169b 100.00%
1572b   30.41%
3376b   65.31%
1012b   19.58%
3081b   59.61%
995b   19.25%
3399b   65.76%
1019b   19.71%
094.css
gzipped
6590b 100.00%
1547b   23.47%
4424b   67.13%
1063b   16.13%
3680b   55.84%
1023b   15.52%
4437b   67.33%
1067b   16.19%
095.css
gzipped
3881b 100.00%
1380b   35.56%
2672b   68.85%
975b   25.12%
2686b   69.21%
972b   25.05%
2672b   68.85%
973b   25.07%
096.css
gzipped
7719b 100.00%
1317b   17.06%
5451b   70.62%
921b   11.93%
3416b   44.25%
833b   10.79%
5451b   70.62%
921b   11.93%
097.css
gzipped
6287b 100.00%
1729b   27.50%
4503b   71.62%
1236b   19.66%
4379b   69.65%
1236b   19.66%
4501b   71.59%
1228b   19.53%
098.css
gzipped
3877b 100.00%
1229b   31.70%
2406b   62.06%
790b   20.38%
2360b   60.87%
800b   20.63%
2406b   62.06%
790b   20.38%
099.css
gzipped
9849b 100.00%
2592b   26.32%
6261b   63.57%
1774b   18.01%
6358b   64.55%
1778b   18.05%
6291b   63.87%
1775b   18.02%
100.css
gzipped
4140b 100.00%
1621b   39.15%
2823b   68.19%
1069b   25.82%
2853b   68.91%
1058b   25.56%
2817b   68.04%
1056b   25.51%
101.css
gzipped
6222b 100.00%
1359b   21.84%
4269b   68.61%
963b   15.48%
4077b   65.53%
953b   15.32%
4264b   68.53%
967b   15.54%
102.css
gzipped
4665b 100.00%
1642b   35.20%
3341b   71.62%
1123b   24.07%
3348b   71.77%
1126b   24.14%
3347b   71.75%
1123b   24.07%
103.css
gzipped
6615b 100.00%
1987b   30.04%
4916b   74.32%
1400b   21.16%
4821b   72.88%
1397b   21.12%
4915b   74.30%
1399b   21.15%
104.css
gzipped
3736b 100.00%
1222b   32.71%
2690b   72.00%
838b   22.43%
2586b   69.22%
827b   22.14%
2690b   72.00%
838b   22.43%
105.css
gzipped
4227b 100.00%
1353b   32.01%
3033b   71.75%
952b   22.52%
2776b   65.67%
947b   22.40%
3040b   71.92%
960b   22.71%
106.css
gzipped
4869b 100.00%
1912b   39.27%
2402b   49.33%
794b   16.31%
2372b   48.72%
800b   16.43%
2402b   49.33%
793b   16.29%
107.css
gzipped
4464b 100.00%
1186b   26.57%
2784b   62.37%
718b   16.08%
2232b   50.00%
705b   15.79%
2886b   64.65%
793b   17.76%
109.css
gzipped
4860b 100.00%
1489b   30.64%
3581b   73.68%
1095b   22.53%
3296b   67.82%
1082b   22.26%
3584b   73.74%
1088b   22.39%
110.css
gzipped
4977b 100.00%
1802b   36.21%
3105b   62.39%
1008b   20.25%
3019b   60.66%
986b   19.81%
3106b   62.41%
1007b   20.23%
111.css
gzipped
6508b 100.00%
1686b   25.91%
4792b   73.63%
1211b   18.61%
4422b   67.95%
1205b   18.52%
4800b   73.76%
1213b   18.64%
112.css
gzipped
4878b 100.00%
1472b   30.18%
3305b   67.75%
965b   19.78%
3240b   66.42%
968b   19.84%
3305b   67.75%
965b   19.78%
113.css
gzipped
4709b 100.00%
1358b   28.84%
3008b   63.88%
850b   18.05%
2730b   57.97%
840b   17.84%
3008b   63.88%
850b   18.05%
114.css
gzipped
4357b 100.00%
1436b   32.96%
2956b   67.84%
966b   22.17%
2862b   65.69%
963b   22.10%
2952b   67.75%
958b   21.99%
115.css
gzipped
8194b 100.00%
1770b   21.60%
5773b   70.45%
1329b   16.22%
5246b   64.02%
1290b   15.74%
5818b   71.00%
1339b   16.34%
116.css
gzipped
4416b 100.00%
1489b   33.72%
2923b   66.19%
969b   21.94%
2800b   63.41%
967b   21.90%
2923b   66.19%
969b   21.94%
117.css
gzipped
6959b 100.00%
1730b   24.86%
4451b   63.96%
1101b   15.82%
3975b   57.12%
1049b   15.07%
4451b   63.96%
1101b   15.82%
118.css
gzipped
7893b 100.00%
2026b   25.67%
5012b   63.50%
1217b   15.42%
4803b   60.85%
1160b   14.70%
5096b   64.56%
1221b   15.47%
119.css
gzipped
5923b 100.00%
1836b   31.00%
4205b   70.99%
1294b   21.85%
4146b   70.00%
1285b   21.70%
4217b   71.20%
1296b   21.88%
120.css
gzipped
4776b 100.00%
1524b   31.91%
2648b   55.44%
867b   18.15%
2549b   53.37%
865b   18.11%
2656b   55.61%
871b   18.24%
121.css
gzipped
4208b 100.00%
1570b   37.31%
2133b   50.69%
809b   19.23%
2144b   50.95%
808b   19.20%
2139b   50.83%
809b   19.23%
122.css
gzipped
7929b 100.00%
2763b   34.85%
4240b   53.47%
1314b   16.57%
4182b   52.74%
1270b   16.02%
4220b   53.22%
1294b   16.32%
123.css
gzipped
6057b 100.00%
1707b   28.18%
4307b   71.11%
1223b   20.19%
4284b   70.73%
1209b   19.96%
4310b   71.16%
1225b   20.22%
124.css
gzipped
5284b 100.00%
1585b   30.00%
3533b   66.86%
1035b   19.59%
3401b   64.36%
1032b   19.53%
3539b   66.98%
1034b   19.57%
125.css
gzipped
4197b 100.00%
1530b   36.45%
2941b   70.07%
1045b   24.90%
2935b   69.93%
1032b   24.59%
2939b   70.03%
1047b   24.95%
126.css
gzipped
4727b 100.00%
1641b   34.72%
3132b   66.26%
1088b   23.02%
2870b   60.72%
1021b   21.60%
3110b   65.79%
1073b   22.70%
127.css
gzipped
5482b 100.00%
1577b   28.77%
3739b   68.21%
1125b   20.52%
3640b   66.40%
1141b   20.81%
3739b   68.21%
1125b   20.52%
128.css
gzipped
4777b 100.00%
1777b   37.20%
2856b   59.79%
984b   20.60%
2838b   59.41%
976b   20.43%
2859b   59.85%
986b   20.64%
130.css
gzipped
5115b 100.00%
1587b   31.03%
3650b   71.36%
1033b   20.20%
3503b   68.48%
1035b   20.23%
3674b   71.83%
1041b   20.35%
131.css
gzipped
4886b 100.00%
1533b   31.38%
3537b   72.39%
1064b   21.78%
3475b   71.12%
1049b   21.47%
3543b   72.51%
1065b   21.80%
132.css
gzipped
3946b 100.00%
1212b   30.71%
2688b   68.12%
805b   20.40%
2660b   67.41%
801b   20.30%
2688b   68.12%
805b   20.40%
133.css
gzipped
4759b 100.00%
1556b   32.70%
3499b   73.52%
1115b   23.43%
3424b   71.95%
1125b   23.64%
3499b   73.52%
1112b   23.37%
134.css
gzipped
4887b 100.00%
1641b   33.58%
3945b   80.72%
1257b   25.72%
3885b   79.50%
1259b   25.76%
3945b   80.72%
1257b   25.72%
135.css
gzipped
5021b 100.00%
1650b   32.86%
3512b   69.95%
1168b   23.26%
3531b   70.32%
1157b   23.04%
3524b   70.19%
1158b   23.06%
136.css
gzipped
4496b 100.00%
1333b   29.65%
2903b   64.57%
841b   18.71%
2797b   62.21%
825b   18.35%
2903b   64.57%
841b   18.71%
137.css
gzipped
6234b 100.00%
1660b   26.63%
4097b   65.72%
1038b   16.65%
3640b   58.39%
1028b   16.49%
4102b   65.80%
1041b   16.70%
138.css
gzipped
5842b 100.00%
1307b   22.37%
4494b   76.93%
913b   15.63%
3129b   53.56%
882b   15.10%
4494b   76.93%
913b   15.63%
139.css
gzipped
3556b 100.00%
1090b   30.65%
2498b   70.25%
669b   18.81%
2254b   63.39%
657b   18.48%
2501b   70.33%
670b   18.84%
140.css
gzipped
5025b 100.00%
1475b   29.35%
3936b   78.33%
1091b   21.71%
3882b   77.25%
1091b   21.71%
3942b   78.45%
1091b   21.71%
141.css
gzipped
3780b 100.00%
1480b   39.15%
2582b   68.31%
977b   25.85%
2446b   64.71%
966b   25.56%
2577b   68.17%
975b   25.79%
142.css
gzipped
4708b 100.00%
1610b   34.20%
3389b   71.98%
1115b   23.68%
3335b   70.84%
1103b   23.43%
3389b   71.98%
1115b   23.68%
143.css
gzipped
4608b 100.00%
1516b   32.90%
3009b   65.30%
1038b   22.53%
3037b   65.91%
1036b   22.48%
3009b   65.30%
1038b   22.53%
144.css
gzipped
4323b 100.00%
1479b   34.21%
2704b   62.55%
945b   21.86%
2705b   62.57%
947b   21.91%
2700b   62.46%
944b   21.84%
145.css
gzipped
4813b 100.00%
1589b   33.01%
3083b   64.06%
1047b   21.75%
2922b   60.71%
1021b   21.21%
3072b   63.83%
1032b   21.44%
146.css
gzipped
4363b 100.00%
1542b   35.34%
3040b   69.68%
960b   22.00%
2782b   63.76%
959b   21.98%
3040b   69.68%
954b   21.87%
147.css
gzipped
5672b 100.00%
1658b   29.23%
4430b   78.10%
1197b   21.10%
3984b   70.24%
1181b   20.82%
4456b   78.56%
1204b   21.23%
148.css
gzipped
9479b 100.00%
2701b   28.49%
5040b   53.17%
1183b   12.48%
4695b   49.53%
1159b   12.23%
5040b   53.17%
1183b   12.48%
149.css
gzipped
5250b 100.00%
1545b   29.43%
3344b   63.70%
1035b   19.71%
3298b   62.82%
1044b   19.89%
3347b   63.75%
1036b   19.73%
150.css
gzipped
6514b 100.00%
1571b   24.12%
4541b   69.71%
1065b   16.35%
4437b   68.11%
1051b   16.13%
4577b   70.26%
1072b   16.46%
151.css
gzipped
5122b 100.00%
1539b   30.05%
3829b   74.76%
1148b   22.41%
3681b   71.87%
1120b   21.87%
3818b   74.54%
1140b   22.26%
152.css
gzipped
4885b 100.00%
1487b   30.44%
3600b   73.69%
1014b   20.76%
3515b   71.95%
1024b   20.96%
3604b   73.78%
1015b   20.78%
153.css
gzipped
4889b 100.00%
1527b   31.23%
3625b   74.15%
1134b   23.19%
3671b   75.09%
1151b   23.54%
3625b   74.15%
1134b   23.19%
154.css
gzipped
5709b 100.00%
1690b   29.60%
4435b   77.68%
1210b   21.19%
3989b   69.87%
1192b   20.88%
4461b   78.14%
1216b   21.30%
155.css
gzipped
3680b 100.00%
1403b   38.13%
2308b   62.72%
829b   22.53%
2307b   62.69%
835b   22.69%
2314b   62.88%
828b   22.50%
156.css
gzipped
4838b 100.00%
1615b   33.38%
3458b   71.48%
1169b   24.16%
3478b   71.89%
1180b   24.39%
3458b   71.48%
1169b   24.16%
157.css
gzipped
3895b 100.00%
1460b   37.48%
2582b   66.29%
795b   20.41%
2464b   63.26%
797b   20.46%
2582b   66.29%
795b   20.41%
158.css
gzipped
4248b 100.00%
1563b   36.79%
2789b   65.65%
1028b   24.20%
2813b   66.22%
1027b   24.18%
2789b   65.65%
1028b   24.20%
159.css
gzipped
5431b 100.00%
1582b   29.13%
3280b   60.39%
1067b   19.65%
3237b   59.60%
1061b   19.54%
3283b   60.45%
1068b   19.66%
160.css
gzipped
4843b 100.00%
1511b   31.20%
3914b   80.82%
1094b   22.59%
3776b   77.97%
1098b   22.67%
3914b   80.82%
1094b   22.59%
161.css
gzipped
4287b 100.00%
1718b   40.07%
2551b   59.51%
1048b   24.45%
2530b   59.02%
1038b   24.21%
2551b   59.51%
1048b   24.45%
162.css
gzipped
7440b 100.00%
2325b   31.25%
4890b   65.73%
1304b   17.53%
4402b   59.17%
1273b   17.11%
4888b   65.70%
1303b   17.51%
163.css
gzipped
6034b 100.00%
1476b   24.46%
4243b   70.32%
1064b   17.63%
3734b   61.88%
1045b   17.32%
4246b   70.37%
1063b   17.62%
164.css
gzipped
3407b 100.00%
1296b   38.04%
2144b   62.93%
761b   22.34%
2023b   59.38%
749b   21.98%
2144b   62.93%
760b   22.31%
165.css
gzipped
4405b 100.00%
1558b   35.37%
3000b   68.10%
1048b   23.79%
3022b   68.60%
1038b   23.56%
3015b   68.44%
1048b   23.79%
166.css
gzipped
6165b 100.00%
1777b   28.82%
4624b   75.00%
1244b   20.18%
4351b   70.58%
1202b   19.50%
4624b   75.00%
1244b   20.18%
167.css
gzipped
5514b 100.00%
1524b   27.64%
3574b   64.82%
1028b   18.64%
3497b   63.42%
1025b   18.59%
3646b   66.12%
1028b   18.64%
168.css
gzipped
3852b 100.00%
1381b   35.85%
2389b   62.02%
812b   21.08%
2407b   62.49%
813b   21.11%
2389b   62.02%
812b   21.08%
169.css
gzipped
6189b 100.00%
1595b   25.77%
4575b   73.92%
1189b   19.21%
4114b   66.47%
1167b   18.86%
4584b   74.07%
1189b   19.21%
170.css
gzipped
4497b 100.00%
1665b   37.02%
3389b   75.36%
1143b   25.42%
3332b   74.09%
1132b   25.17%
3391b   75.41%
1142b   25.39%
171.css
gzipped
4718b 100.00%
1579b   33.47%
2978b   63.12%
920b   19.50%
2828b   59.94%
918b   19.46%
2976b   63.08%
922b   19.54%
172.css
gzipped
7255b 100.00%
1925b   26.53%
4936b   68.04%
1186b   16.35%
4808b   66.27%
1176b   16.21%
4945b   68.16%
1189b   16.39%
173.css
gzipped
3981b 100.00%
1376b   34.56%
3007b   75.53%
955b   23.99%
2983b   74.93%
961b   24.14%
3007b   75.53%
952b   23.91%
174.css
gzipped
4172b 100.00%
1344b   32.21%
3024b   72.48%
953b   22.84%
3012b   72.20%
937b   22.46%
3024b   72.48%
940b   22.53%
175.css
gzipped
4592b 100.00%
1328b   28.92%
2748b   59.84%
909b   19.80%
2638b   57.45%
914b   19.90%
2753b   59.95%
913b   19.88%
176.css
gzipped
3231b 100.00%
1252b   38.75%
2387b   73.88%
854b   26.43%
2362b   73.10%
856b   26.49%
2390b   73.97%
857b   26.52%
177.css
gzipped
6261b 100.00%
1669b   26.66%
3842b   61.36%
1080b   17.25%
3738b   59.70%
1082b   17.28%
3843b   61.38%
1092b   17.44%
178.css
gzipped
5249b 100.00%
1673b   31.87%
3111b   59.27%
1017b   19.38%
2930b   55.82%
1012b   19.28%
3117b   59.38%
1019b   19.41%
179.css
gzipped
5853b 100.00%
1606b   27.44%
3597b   61.46%
1028b   17.56%
3420b   58.43%
1010b   17.26%
3597b   61.46%
1028b   17.56%
180.css
gzipped
4566b 100.00%
1247b   27.31%
2888b   63.25%
793b   17.37%
2230b   48.84%
749b   16.40%
2894b   63.38%
792b   17.35%
181.css
gzipped
4420b 100.00%
1325b   29.98%
3146b   71.18%
951b   21.52%
3002b   67.92%
950b   21.49%
3146b   71.18%
946b   21.40%
182.css
gzipped
4588b 100.00%
1466b   31.95%
3027b   65.98%
900b   19.62%
2906b   63.34%
898b   19.57%
3027b   65.98%
900b   19.62%
184.css
gzipped
5426b 100.00%
1749b   32.23%
4127b   76.06%
1290b   23.77%
4171b   76.87%
1302b   24.00%
4127b   76.06%
1290b   23.77%
185.css
gzipped
4632b 100.00%
1389b   29.99%
3325b   71.78%
952b   20.55%
3326b   71.80%
958b   20.68%
3320b   71.68%
951b   20.53%
186.css
gzipped
3932b 100.00%
1319b   33.55%
2768b   70.40%
935b   23.78%
2736b   69.58%
928b   23.60%
2786b   70.85%
939b   23.88%
187.css
gzipped
5687b 100.00%
1509b   26.53%
3138b   55.18%
936b   16.46%
2879b   50.62%
908b   15.97%
3138b   55.18%
936b   16.46%
188.css
gzipped
5893b 100.00%
1750b   29.70%
3633b   61.65%
1105b   18.75%
3566b   60.51%
1126b   19.11%
3632b   61.63%
1105b   18.75%
189.css
gzipped
6378b 100.00%
1804b   28.28%
3714b   58.23%
1155b   18.11%
3607b   56.55%
1147b   17.98%
3717b   58.28%
1154b   18.09%
190.css
gzipped
4385b 100.00%
1417b   32.31%
2598b   59.25%
920b   20.98%
2443b   55.71%
920b   20.98%
2604b   59.38%
921b   21.00%
191.css
gzipped
5072b 100.00%
1569b   30.93%
3923b   77.35%
1157b   22.81%
3799b   74.90%
1147b   22.61%
3926b   77.41%
1159b   22.85%
192.css
gzipped
5042b 100.00%
1587b   31.48%
3236b   64.18%
1020b   20.23%
3225b   63.96%
1014b   20.11%
3246b   64.38%
1013b   20.09%
193.css
gzipped
4411b 100.00%
1764b   39.99%
2461b   55.79%
872b   19.77%
2485b   56.34%
873b   19.79%
2465b   55.88%
874b   19.81%
194.css
gzipped
6109b 100.00%
1612b   26.39%
4162b   68.13%
1145b   18.74%
4031b   65.98%
1157b   18.94%
4162b   68.13%
1145b   18.74%
195.css
gzipped
4990b 100.00%
1484b   29.74%
3589b   71.92%
1038b   20.80%
3595b   72.04%
1041b   20.86%
3592b   71.98%
1039b   20.82%
196.css
gzipped
4859b 100.00%
1537b   31.63%
3569b   73.45%
1152b   23.71%
3523b   72.50%
1151b   23.69%
3578b   73.64%
1152b   23.71%
197.css
gzipped
6709b 100.00%
1769b   26.37%
4534b   67.58%
1179b   17.57%
4136b   61.65%
1162b   17.32%
4539b   67.66%
1177b   17.54%
198.css
gzipped
4996b 100.00%
1476b   29.54%
3214b   64.33%
975b   19.52%
3078b   61.61%
959b   19.20%
3211b   64.27%
976b   19.54%
199.css
gzipped
4128b 100.00%
1354b   32.80%
2941b   71.25%
914b   22.14%
2752b   66.67%
917b   22.21%
2987b   72.36%
923b   22.36%
200.css
gzipped
5029b 100.00%
1491b   29.65%
3714b   73.85%
1027b   20.42%
3695b   73.47%
1020b   20.28%
3723b   74.03%
1028b   20.44%
201.css
gzipped
5752b 100.00%
1500b   26.08%
3723b   64.73%
973b   16.92%
3463b   60.21%
956b   16.62%
3723b   64.73%
973b   16.92%
202.css
gzipped
5689b 100.00%
1633b   28.70%
4116b   72.35%
1214b   21.34%
3944b   69.33%
1179b   20.72%
4124b   72.49%
1214b   21.34%
203.css
gzipped
3777b 100.00%
1359b   35.98%
2643b   69.98%
978b   25.89%
2649b   70.14%
968b   25.63%
2642b   69.95%
977b   25.87%
204.css
gzipped
11101b 100.00%
1499b   13.50%
4480b   40.36%
1054b   9.49%
4033b   36.33%
1063b   9.58%
4479b   40.35%
1055b   9.50%
205.css
gzipped
4048b 100.00%
1413b   34.91%
2840b   70.16%
967b   23.89%
2862b   70.70%
977b   24.14%
2840b   70.16%
965b   23.84%
206.css
gzipped
4565b 100.00%
1270b   27.82%
3190b   69.88%
883b   19.34%
3083b   67.54%
882b   19.32%
3190b   69.88%
883b   19.34%
207.css
gzipped
3056b 100.00%
1203b   39.37%
2149b   70.32%
798b   26.11%
2113b   69.14%
798b   26.11%
2152b   70.42%
798b   26.11%
208.css
gzipped
4805b 100.00%
1462b   30.43%
3553b   73.94%
1041b   21.66%
3477b   72.36%
1029b   21.42%
3559b   74.07%
1042b   21.69%
209.css
gzipped
7164b 100.00%
1795b   25.06%
4765b   66.51%
1187b   16.57%
4461b   62.27%
1187b   16.57%
4765b   66.51%
1187b   16.57%
210.css
gzipped
6601b 100.00%
1934b   29.30%
4236b   64.17%
1279b   19.38%
4235b   64.16%
1270b   19.24%
4234b   64.14%
1278b   19.36%
211.css
gzipped
5181b 100.00%
1498b   28.91%
3725b   71.90%
1052b   20.30%
3679b   71.01%
1040b   20.07%
3768b   72.73%
1053b   20.32%
212.css
gzipped
5857b 100.00%
2239b   38.23%
4043b   69.03%
1578b   26.94%
4030b   68.81%
1566b   26.74%
4040b   68.98%
1571b   26.82%
213.css
gzipped
7701b 100.00%
1768b   22.96%
5827b   75.67%
1307b   16.97%
5293b   68.73%
1268b   16.47%
5879b   76.34%
1337b   17.36%
Avg size after min 100% 66.41% 63.29% 66.47%
Avg size gzip+min 30.36% 19.63% 19.44% 19.62%
 

cssmin.js in windows shell

Friday, October 29th, 2010

JavaScript can run virtually anywhere, including as a windows exe and the windows command line.

Say you have a JavaScript function foo()

function foo(input) {
  var output = input;
  // .. unicorns
  return output;
}

In order to make this a windows shell script you add at the and a way to read standard input and then write to the standard output:

(function () {
 
    var input  = WScript.StdIn.ReadAll(),
        output = foo(input);
 
    WScript.StdOut.Write(output);
 
}());

Then you run this script, say foo.js, like:

$ cscript foo.js < input.txt

And it prints the output to the console.

If you want to read and print the code of foo.js itself you go:

$ cscript foo.js < foo.js
Microsoft (R) Windows Script Host Version 5.7
Copyright (C) Microsoft Corporation. All rights reserved.

function foo(input) {
  var output = input;
...

You can remove this "Microsoft (R) Windows..." stuff with //NoLogo parameter:

$ cscript //NoLogo foo.js < foo.js
function foo(input) {
  var output = input;
...

CSSMin.JS

Alrighty, going back to the title of the post.

CSSMin.js is a port of YUICompressor's CSS minifier (source, hosted tool). Now adding a few lines at the end makes a windows shell script:

(function () {
 
    var src = WScript.StdIn.ReadAll();
 
    if (!src) {
        // help!
        WScript.StdOut.WriteLine("cscript //NoLogo cssmin.js < in.css > out.css");
        return;
    }
 
    WScript.StdOut.Write(YAHOO.compressor.cssmin(src));
 
}());

Download it here.

Use it like:

$ cscript //NoLogo cssmin.js < in.css > out.css

Don't forget the //NoLogo or you'll end up with "Microsoft..." in your minified files

Random observation: "dude"[0] === "d" in most JS environments but is undefined in WSH (Windows Scripting Host). So "dude".substring(0, 1)

 

Preload, then execute

Saturday, October 23rd, 2010

I talked before about using an object element to load scripts and styles without executing/applying them to the current document. And this is nice for preloading assets - faster and less error-prone than simple inclusion if the document or iframe.

But what about preloading assets (scripts) and then executing them on the current page? Turns out it works just fine, like so:

  • create an object element and point its data to the JavaScript file to be loaded
  • listen to the load event of the object
  • once loaded, create a script element and point its src to the file, which would be already cached
  • optionaly, execute a callback function once the script element is properly inserted into the DOM

Something like this:

function yield(file, callback) {
    var o = document.createElement('object');
    o.data = file;
        
    o.width  = 0;
    o.height = 0;
    
    o.onload = function() {
        var s = document.createElement('script');
        s.src = file;
        s.onload = function() {
            callback(o, file);
        };
        document.getElementsByTagName('head')[0].appendChild(s);
        
    };
    
    document.body.appendChild(o);
}

But, but.. why?

Well, first off it's yet another way to load JavaScript files asynchronously, in a non-blocking fashion. And that's a good thing. Async = win. This method sure beats others such as document.write (eek) or iframe. It has advantage over XHR because there's no same-domain restriction. DOM include is probably the only simpler and more robust method, but there are cases where the object element might be preferable.

DOM-included scripts in FF and Opera always execute in order. That's cool, but sometimes you may prefer out of order async execution. Whenever a script is ready - go! Or sometimes you may prefer if the script is not executed at all! That may sound odd (why would I load it if I don't want to execute it?) but I have a use case - auto-complete. When your data source is on another domain and you can't use XHR, then JSON-P would be the way to go. But the network is a jungle and anything can happen.

Say you make requests for auto-complete suggestions with queries "h", "he", "hel", "hell", "hello". For some reason "hel" is really slow and you already have the results for "hell" and even "hello". What you want to do is just kill and cancel the "hel" response, who cares - it's outdated already. But if you use JSONP in FF and Opera "hel" will block the others. If order is not preserved it might be even weirder - you update the page with the "hello" suggestion and then "hel" finally arrives and updates the suggestion once again. Fail.

Using the object element approach you can keep a list of requests that went out and simply choose to ignore previous requests if newer responses are already there.

I have a test case even.

Small print

This technique assumes that the scripts you load don't have any headers preventing caching, otherwise they'll be requested twice.

My example code above ignored IE completely, but you can easily make it work, either using new Image() instead of object or tweaking the object with height and width of 1 as shown in the old article. Or you can use the trick that LABjs uses (I think) which is to DOM-insert a script element with invalid type such as cache/javascript. Also you need to listen to onreadystatechange instead of load event when inserting a DOM element.

 

Inline MHTML+Data URIs

Sunday, October 3rd, 2010

MHTML and Data URIs in the same CSS file is totally doable and gives us nice support for IE6+ and all modern browsers. But the question is - what about inline styles. In other words can you have a single-request web application which bundles together markup, inline styles, inline scripts, inline images? With data URIs - yes, clearly. But MHTML?

I remember hacker extraordinaire Hedger Wang coming up with a test page, which proved it's doable. Problems with the test are that a/ I can't find the page anymore, his domain has expired b/ there was some funky IE7/Vista stuff (probably now solvable) in there which even included an undesired redirect c/ was complex - the whole HTML becomes a multipart document, if I remember correctly there was something that required html served as text/plain....

So I tried something simple - shove an MHTML doc inside an inline style comment. It so totally worked! Including IE6 and IE8 in IE7 mode on Windows 7 (which in my experience behaves as badly as IE7 proper on Vista)

Here's the test page. Look ma', no extra HTTP requests :)

So it's a simple HTML doc:

<!doctype html>
<html>
  <head>
    <title>Look Ma' No HTTP requests</title>
    <style type="text/css">
 
/* magic here */
 
    </style>
  </head>
  <body>
    <h1>MHTML + Data:URIs inline in a <code>style</code> element</h1>
    <p class="image1">hello<br>hello</p>
    <p class="image2">bonjour<br>bonjour</p>
  </body>
</html>

And the magic is two parts: the MHTML doc inside a CSS comment and the actual CSS which uses data URIs for normal browsers and refers to the MHTML parts in IE6,7.

/*
Content-Type: multipart/related; boundary="_"
 
--_
Content-Location:locoloco
Content-Transfer-Encoding:base64
 
iVBORw0KGgoAAAAN ... [more crazyness]... QmCC
--_
Content-Location:polloloco
Content-Transfer-Encoding:base64
 
iVBORw0KGgoAAAANSUh ... [moarrr] ... ggg==
--_--
*/
.image1 {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAAN ... QmCC"); 
  *background-image: url(mhtml:http://phpied.com/files/mhtml/mhtml-html.html!locoloco); 
}
 
.image2 {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUh ... ggg=="); 
  *background-image: url(mhtml:http://phpied.com/files/mhtml/mhtml-html.html!polloloco); 
}
 
body {
  font: bold 24px Arial;
}

How cool is that!

Please report any issues you might find in any browser/os combination

The obvious drawback is repeating the long base64'd image content twice, but it's solvable with either server-side sniffing or... one crazy hack, found on the Russian site habrahabr.ru. I should talk about it separately and help spread the word to the larger English-speaking audience, but for the impatient - click!

So there you go - MHTML inline in CSS inline in HTML or building single-request x-browser web apps :)

 

The proper MHTML syntax

Sunday, October 3rd, 2010

Reducing the number of HTTP requests is a must, sprites are cool, but a pain to maintain, so there come data URIs (for all browsers) and MHTML (IE6 and 7). I've talked about these things on this blog to a point where the blog comes up in top 10 results in search engines for queries like "mhtml" and "data url". Therefore I think it's my duty to clarify a point for the good of the mankind :)

MHTML works in IE6 and IE7 even in the deadly IE7/Vista and IE7/Win7 combos

In the community we've long considered MHTML in IE7/Vista a problem and I've personally come up with complex voodoo workarounds how to mitigate the issue and still make use of the technique. Turns out the whole problem all the time was a small syntax glitch.

Pointed by a comment at a previous post all we ever needed was to close the boundary delimiter and add two dashes at the end. The double-dash of doom as I like to call them since I've spent so much time wrestling.

So let's take a look at the syntax.

Update: Also check this comment for additional insight from Vincent, Aaron and _cphr_ regarding double line break of doom

One part

MHTML is a multi-part document. One document containing several parts. One part looks like this:

Content-Location: myimage
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSU....U5ErkJggg==

In other words it has headers, base64-encoded content and two empty lines to divide them.

Multi parts

The different parts in the document are divided by a separator string. And at the top of the document you define what this separator is. Anything you like. So

Content-Type: multipart/related; boundary="MYSEPARATOR"

--MYSEPARATOR

[here comes part one]

--MYSEPARATOR

[here's part two]

--MYSEPARATOR--

Did you notice -- at the very end? Yes, this is the double-dash of doom. Forget it and you get IE7/Vista problems (only on cached documents) and permanent hair loss. The thing is that in IE6 and other IE7s you can omit the whole last separator and it's all good. So historically you never needed it, but come Vista and Win7 and problems start.

All together now

Finally, let's see the whole thing, a whole CSS file, including the way you refer to the parts later on in the CSS.

/*
Content-Type: multipart/related; boundary="MYSEPARATOR"
 
--MYSEPARATOR
Content-Location: myimage
Content-Transfer-Encoding: base64
 
iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAD....U5ErkJggg==

--MYSEPARATOR
Content-Location: another
Content-Transfer-Encoding: base64
 
iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAA....U5ErkJggg==

--MYSEPARATOR--
*/
.myclass {
    background-image:url(mhtml:http://example.org/styles.css!myimage);
}
.myotherclass {
    background-image:url(mhtml:http://example.org/styles.css!another);
}

Updated PHP class

Previously when I fought IE7/Vista I came up with a PHP class that would take some images and create "data sprites" on the fly, creating two different versions - one with data URIs and one with MHTML, depending on the browser. The old code is here.

Now, I've updated it (basically just deleted serious portions of it that dealt with Vista) and put it up on github. Right here.

Updated test pages

Thanks for reading, that's about it. Now off I go to correct the older posts, catch ya later :)

 

The European Tour 2010

Saturday, October 2nd, 2010

So it's been pretty quiet around here. I'm still alive and very much so. Just awfully busy.

Bulgaria

I took a long trip in Bulgaria. About 1 month and 20 days. Including the traditional around-Bulgaria let's-get-drunk-in-different-cities-every-day tour de force with the gang. With unusual amount of guitar playing this time.

Not all fun and roses, though, I had to work most of the time for a superimportant gamechanging ultrasecret project for Yahoo! Search. And work on the finishing touches for a book.

France, Italy, UK

While spending time in Bulgaria I did some quick jumps to other European countries - France, Italy, UK. Everything is so close in Europe, it's a shame not to wander around once you cross the big pond.

In Paris I had the pleasure to practice some French, but most importantly to meet with the local speed freaks at the Web Perf Meetup organized by Éric Daspet. Great experience to interrupt sightseeing and soufflé-eating just to be among fellow geeks in a small and cozy group.

web perf meetup paris

photo: @tbassetto

Second tour leg

Back in California to the normal life and kids' school year start end of August. But a second European tour leg was just around the corner.

JSConfEU in Berlin was last week, this is hands-down the best conference. I cannot speak high enough about it, the organizers, the people, speakers, everyone, the parties (ouch, the parties). I met so many people I have been in contact with (or wanted to be) for quite a while, including the new book's tech reviewer Andrea aka @WebReflection. It's surprising, for the regular introvert geek I am, the pleasure of meeting and talking to people. Oh, the miracles of the little, or not so little, quantities of Berliner Pilsner.

Back from Berlin, a copy of the new book was waiting, how adorable! (I really need to put some marketing effort into it... at least put up a page what's it about, since people are asking). It's been rating very well in Amazon's "JavaScript" category. Saw it at #3 a few times, watch out "good parts"!

Next week it's Fronteers conference in Amsterdam. Really looking forward to this one. After all, the one and only PPK started it all. I'll be speaking about progressive downloads and progressive rendering. Progress is important, progress is critical! And I've never been to Amsterdam.

Then Moscow and highload++ conference. How cool that would be. I always wanted to see Moscow or any part of Russia really.

So... busy, busy, busy. And another book baby on the road. And one last conference but here in LA's "backyard" Las Vegas this time.

 

Conditional comments block downloads

Sunday, May 23rd, 2010

I came across this blog post (via @pornelski and @souders) where Markus has stumbled upon a case where an IE6-only stylesheet included with a conditional comment blocks the downloads in IE8. Whaaat?

I had to dig in. To give you a summary: turned out that any conditional comment, not only for an extra CSS, will block further downloads until the main CSS file arrives. Also the solution offered on the blog post (using X-UA-Compatible) seems to be more of an error due to an accidentally left comment.

Check out the tests.

Base page

The first test is the base page. It follows a pretty common style pattern - CSS at the top, a bunch of images in the middle, JS at the bottom.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd">
    
<html>
<head>
    <title>The base page</title>
    <link type="text/css" rel="stylesheet" 
          href="http://tools.w3clubs.com/pagr/1.expires.css">
</head>
<body>
<p>
    <img src="http://tools.w3clubs.com/pagr/1.expires.png" alt="1">
    <img src="http://tools.w3clubs.com/pagr/2.expires.png" alt="2">
    <img src="http://tools.w3clubs.com/pagr/3.expires.png" alt="3">
    <img src="http://tools.w3clubs.com/pagr/4.expires.png" alt="4">
</p>
<script type="text/javascript" 
        src="http://tools.w3clubs.com/pagr/1.expires.js"></script>
</body>
</html>

The waterfall produced by WebPageTest looks like so:

base page

The page is here, the results of the WebPageTest's test are here.

Conditional IE6 stylesheet

Adding a second stylsheet to the head like so:

<head>
  <title>base page</title>
  <link type="text/css" rel="stylesheet" 
        href="http://tools.w3clubs.com/pagr/1.expires.css">
  <!--[if IE 6 ]>    
    <link type="text/css" rel="stylesheet" 
          href="http://tools.w3clubs.com/pagr/2.expires.css">
  <![endif]-->
</head>

Turns out that this conditional comment blocks further downloads until the main CSS arrives.

Test page, test results, waterfall:

CC style page

Just like that the total page to onload time went up from 1 second to almost 1.3 seconds. Ouch.

And this is because of an IE6 stylesheet, which IE8 has no use for. My wild guess is that IE needs to parse through those conditional comments and treats them sort of like inline script. And we know that inline scripts following a stylesheet tend to block.

Conditional markup

What if we don't include IE6 specific stylsheet, but use the conditional comments to write different body tags with different class names, as described by Paul Irish.

<!--[if IE 6]> <body class="ie6"> <![endif]--> 
<!--[if !IE]><!--> <body> <!--<![endif]-->

Turns out that these markup conditional comments will also block downloads until the CSS arrives.

Test page, test results, waterfall looks as bad (the same blocking).

Browser-sniffing comments

I blogged about this a few days ago - using conditional comments to do the browser sniffing and include appropriate CSS - one for normal browsers and a complete alternative for IE6,7.

<head>
  <!--[if lte IE 7]>
    <link type="text/css" rel="stylesheet" 
          href="http://tools.w3clubs.com/pagr/2.expires.css">
  <![endif]-->
  <!--[if gt IE 7]><!-->
    <link type="text/css" rel="stylesheet" 
          href="http://tools.w3clubs.com/pagr/1.expires.css">
  <!--<![endif]-->
</head>

Turns out this is OK to do. The conditional comments are processed before the download is initiated, so there't nothing to block on after the stylesheet. Yeey!

Test page, test results, waterfall looks like the first one for the base page.

X-UA-Compatible not a solution

The blog post suggested using the X-UA-Compatible meta tag to say that the UA is the latest IE possible.

<meta http-equiv="X-UA-Compatible" content="IE=edge">

It didn't work for me.

Test page, test results, waterfall like the second (the blocking) one.

Looking closely and thanks to the screenshots digging out the original test page I noticed that it contains a dangling comment. Putting that dangling comment in a test page, and the blocking effect was gone. But this is a bug. In fact the comment shows up on the page! My wild guess is that this improper comments invalidates the following one and that's why there's no blocking. I guess that IE6 will not load the stylesheet, but I didn't test.

So my tests - with X-UA meta tag (results) and with comment bug (results)

Conclusions, conclusions

To summarize, if you worry about performance, don't use conditional comments.

There might be an exception when you put a script in the head after the CSS - then there are two blocking things, so the effect of the conditional comment is not visible. But that's still bad for performance, there's still blocking.

It's OK to use the browsers-sniffing comments approach, provided of course, that there's no other CSS file before the sniff. If there is one, it will block.

Best - just use _ and * hacks, go with a single CSS and forget sniffing.

Update: empty conditional comment

Thanks to Markus who kept looking into the extra conditional comment comes a solution: an empty conditional comment early on solves the blocking issue.. By "early on" I mean before the main CSS which caused the blocking.

I did two more tests to validate the solution and it absolutely works.

One test has empty comment + conditional comment for writing Paul's body tags. The empty comment (aka the solution) is right before the blocking CSS file.

<head>
    <title>base page</title>
    <!--[if IE 6]><![endif]-->
    <link type="text/css" rel="stylesheet" 
          href="http://tools.w3clubs.com/pagr/1.expires.css">
</head>
 
<!--[if IE 6]> <body class="ie6"> <![endif]-->
<!--[if !IE]><!--> <body> <!--<![endif]-->

Test page, webpagetest results. Works as advertised, no more blocking.

The second test uses empty comment and conditional stylesheet. In this case I even put the empty comment way at the top. Sort of like declaring upfront - hey this page uses conditional comments and the empty comment is the solution to the blocking effect.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd">
<!--[if IE 6]><![endif]-->
 
<html>
<head>
  <title>base page</title>
  <link type="text/css" rel="stylesheet" 
        href="http://tools.w3clubs.com/pagr/1.expires.css">
  <!--[if IE 6 ]>    
    <link type="text/css" rel="stylesheet" 
          href="http://tools.w3clubs.com/pagr/2.expires.css">
  <![endif]-->
</head>

Test page, webpagetest results. No more blocking :)

Answering Andrea's question - what it if you have several conditionals - for IE6, IE7 and so on, I did one more test with two conditional CSS files. Turns out it's fine, as long as there's one conditional comment before the CSS. I actually updated the comment so it checks for all IE versions.

Test page, results, code:

<!--[if IE]><![endif]-->
<html lang="en">
<head>
    <title>base page</title>
    <link type="text/css" rel="stylesheet" 
          href="http://tools.w3clubs.com/pagr/1.expires.css">
    <!--[if IE 6]>    
        <link type="text/css" rel="stylesheet" 
              href="http://tools.w3clubs.com/pagr/2.expires.css">
    <![endif]-->
    <!--[if IE 7]>    
        <link type="text/css" rel="stylesheet" 
              href="http://tools.w3clubs.com/pagr/3.expires.css">
    <![endif]-->
</head>

In conclusion #2... conditional comments cause CSS to block. The workaround it to have an extra empty comment before the blocking CSS, or, to be safe, right after the doctype. Even better - don't use conditional comments at all. Except for browser-sniffing and loading two complete and completely separate CSS files, not just the IE fixes.

 

YUI CSS min – part 3 – hacks

Friday, May 21st, 2010

The previous parts are here (building and testing) and here (what gets minified). Now let's see how YUI CSS min handles CSS hacks.

As you know CSS hacks often use errors in CSS parsers in browsers to target specific browser versions and supply additional rules to work around other issues in said browsers. That makes any CSS tool's job slightly more challenging. Not only does the tool have to avoid repeating the browsers errors, but also has to understand what browsers got wrong and support it too. Fun stuff. Isn't it a joy being a web developer?

So here are some hacks that are tested to work with the YUICopmpressor's CSS min.

Underscore/star hack

The simplest ever hack to target IE6 and IE7. In the example below normal browsers see 1px dropping _width and *width as invalid, IE7 ignores the *, drops the _width as invalid and sees 3pt, IE6 ignores the _ and sees _width as width, so it sees 2em.

CSS min doesn't parse and doesn't understand CSS properties, so it accepts pretty much any property.

Before:

#element {
    width: 1px;
    *width: 3pt;
    _width: 2em;
}

After:

#element{width:1px;*width:3pt;_width:2em}

Child selector hack

CSS min strips comments but there is this child selector hack people use to hide declarations from IE7 and below.

CSS min retains empty comments that immediately follow > (thanks go out to Chris Burroughs)

Before:

html >/**/ body p {
    color: blue; 
}

After:

html>/**/body p{color:blue}

IE5/Mac hack

This hack targets IE5/Mac, if anyone still worries about this browser. The hack is retained after minification, only it's minified.

Before:

/* Ignore the next rule in IE mac \*/
.selector {
   color: khaki;
}
/* Stop ignoring in IE mac */

After:

/*\*/.selector{color:khaki}/**/

Box model hack

This hack uses valid CSS and there's no special use of comments so it's retained.

Before:

#elem { 
    width: 100px; /* IE */
    voice-family: "\"}\""; 
    voice-family:inherit;
    width: 200px; /* others */
}
html>body #elem {
    width: 200px; /* others */
}

After:

#elem{width:100px;voice-family:"\"}\"";voice-family:inherit;width:200px}html>body #elem{width:200px}

Seems like the code highlighter chokes here though. It ain't easy :)

That's all, folks!

Thanks and please, feel free to suggest improvements and report bugs. Also play with the web UI of the JS-version here to see for yourself what it does to your code.

 

YUI CSS Min – part 2

Thursday, May 20th, 2010

The first part is here. It was more about building the YUICompressor, writing and running test cases. Now let's see what the compressor does exactly to your CSS.

BTW, you can play with the web UI to see for yourself how the minifier works.

Stripping comments and white space

This is the bare minimum a minifier can do. And when it comes to CSS, this is also the place where ther biggest improvement comes from. In JS for example you can rename variables and save bytes, but in CSS the possibilities are more limited. No shorter way to say text-decoration, unfortunately.

So before:

/***** 
  classmates stuff
*****/
.classmates {
    /* after 10 years */
    weight: considerable;
}

After:

.classmates{weight:considerable}

Special comments

Stripping comments is nice but not always ok. Sometimes you need to retain copyright information. Use ! at the beginning of the comment to mark the comment as special.

Before:

/*!
  (c) copyright copyleft
*/
.classmates {
    /* after 10 years */
    weight: considerable;
}

After:

/*!
  (c) copyright copyleft
*/.classmates{weight:considerable}

Thanks to the charmingly insisting Billy Hoffman and the valid case he presented, the bang (!) itself is preserved too. This way you can safely double minify. Also lint tools (such as Zoompf, YSlow and PageSpeed) can see the ! and conclude that this comment is there intentionally, not because you forgot to minify.

Striping last semi-colon

The last semi-colon in a declaration block is out. So keep it in your source for maintenance purposes and let the minifier take care of stripping it out.

Before:

a {
  one: 1;
  two: 2;
}

After:

a{one:1;two:2}

Extra semi-colons

One semi-colon is all you need, so the minifier will strip an accidentally added one.

Before:

p :link { 
  ba: zinga;;;
  foo: bar;;;
}

After:

p :link{ba:zinga;foo:bar}

No empty declarations

Empty declaration blocks don't do anything, so why send them over the net?

Before:

.empty { ;}

After:

(nothing...)

Zero values

A zero is a zero. Zero pixels or % or centimeters or whatever, it's still zero. Also sometimes (when everything is a zero) you need just one zero instead of four, or three or two.

Before:

a { 
  margin: 0px 0pt 0em 0%;
  background-position: 0 0ex;
  padding: 0in 0cm 0mm 0pc
}

After:

a{margin:0;background-position:0 0;padding:0}

Floats

For values such as 0.something, the 0 is not needed.

Before:

::selection { 
  margin: 0.6px 0.333pt 1.2em 8.8cm;
}

After:

::selection{margin:.6px .333pt 1.2em 8.8cm}

Colors values

RGB color values are nice, but not the most concise form. Make them hex. Also AABBCC hex can be the shorter ABC. But don't touch RGBA and don't touch the IE filter values in quotes.

Before:

.color {
  me: rgb(123, 123, 123);
  impressed: #ffeedd;
  background: none repeat scroll 0 0 rgb(255, 0,0);
}

After:

.color{me:#7b7b7b;impressed:#fed;background:none repeat scroll 0 0 #f00}

Before:

.cantouch {
  alpha: rgba(1, 2, 3, 4);
  filter: chroma(color="#FFFFFF");
}

After (no color minification) :

.cantouch{alpha:rgba(1,2,3,4);filter:chroma(color="#FFFFFF")}

Single charsets

Only one charset is allowed per stylesheet. So, if there's more than one, strip it. It may happen when merging several stylesheets into one.

Before:

@charset "utf-8";
#foo {
  border-width: 1px;
}
 
/* second css, merged */
@charset "another one";
#bar {
  border-width: 10px;
}

After:

@charset "utf-8";#foo{border-width:1px}#bar{border-width:10px}

Alpha opacity

There's a shorter way to write opacity filter for IE.

So before:

code {
   -ms-filter: "PROGID:DXImageTransform.Microsoft.Alpha(Opacity=80)"; /* IE 8 */
   filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);       /* IE 4-7 */
}

After:

code{-ms-filter:"alpha(opacity=80)";filter:alpha(opacity=80)}

There are more filters that could be shorten besides the opacity, but MSDN suggests the longer syntax should be used, So a bit more experimentation is needed here...

Thanks!

Whew, sort of a lengthy post. Thank you for reading and coming up next time... hacks :)

If you have ideas, comments, your bug reports are welcome

 

15 minutes could save you…

Monday, May 17th, 2010

Since I have a ton of things to do, I decided it was about time to spend some time with this blog, performance optimization-wise. Not do anything special, just the bare minimum, the no-brainer, works-every-time, easy stuff. And I'm quite happy with the results.

I only looked at the homepage, although the results will be seen throughout the site. Unfortunately there was a youtube video on the homepage, otherwise the results would have been even better, KB-wise

gzip on

First things first - turning compression on. I've previously whined about the host of this blog, site5.com and how I wasn't able to enable compression for some tests and had to make PHP do the compression. Turned out, all it takes is just ask and open a support ticket. Second level support decided to compile Apache with mod_deflate and I was good to go. I put this in .htaccess:

AddOutputFilterByType DEFLATE text/css text/plain text/xml application/javascript application/json

This blog has no JavaScript, just HTML and CSS. The HTML became 5K (from 23K) and CSS became 3 something (from 11K)

Flush

Decided to go fancy here and do first byte flush early on. That, of course, can be problematic, especially on shared hosting. I mean problematic is to have gzip working together with flushing (tips). Eventually I gave up wrestling with .htaccess and php.ini settings and let PHP handle it. That's why you may notice that text/html is missing from the DEFLATE list above.

So all it took was going to my WordPress theme, finding something called header.php and adding two lines of PHP.

One at the top:

<?php ob_start("ob_gzhandler"); ?>
<!DOCTYPE html ...

And one at the bottom:

<?php ob_flush(); flush(); ?>

Now the header part is flushed in one chunk and the rest in another. Check it in chunkview...

favicon.ico

I have no favicon so I get a lot of 404s, since browsers insist on downloading this little thing. So I created one. Took a social profile photo, croped (Option+K) and played with green colors in Mac's built-in Preview program. Then, ImageMagick:

$ convert -resize 16x16 dude.png PNG8:favicon16.png
$ convert favicon16.png favicon.ico

FTP. Done. No mo' 404s for favicon.

Minifying CSS

Copy-paste into CSSMin and CSS lost 30% of its weight. Now after gzipping and minifying, the CSS went from total 11.3K to 2.6K.

Cover images

I have some book covers on the homepage. One was simply linked to the publisher's site (extra DNS, connection...). Also turned out the publisher has redesigned the site so there was also a redirect. Disaster. Also the image was a 26K PNG where it could be 4k JPEG.

$ convert cover.png cover.png.jpg
$ jpegtran -copy none -optimize cover.png.jpg > cover.jpg

And just like that - 4 book covers were optimized.

Before/after

And that's all I did. I can probably do Expries headers too, convert/sprite all smiley GIFs and so on but that means touching more of WordPress, so I stopped here.

Now the page loads (onload) in 1.2 seconds, down from 2.2 (45% faster).

There are fewer DNS lookups, no 404s, no 301s

Page weight is down from 285K to 186K (34%). Actually if you exclude the youtube 142K SWF, the result is: 143K (before) to 44K (after) or a page weight saving of 70%. Not bad, not bad at all.

And the waterfalls. Before:

After:

PageSpeed score only went from 84/100 to 87/100 which was not impressive at all. I think 84K may have been too generous, but maybe not, given how worse sites there are out there.

So this is it - 15 minutes could save you 45% page load time and 70% download sizes :)

 

Browser sniffing with conditional comments

Thursday, May 13th, 2010

Browser sniffing is bad. But sometimes unavoidable. But doing it on the server is bad, because UA string is unreliable. The solution is to use conditional comments and let IE do the work. Because you're targeting IE most of the times anyway.

In fact IE8 is a decent browser for the most practical purposes and often you're just targeting IE before 8.

Conditional comments in practice use the following pattern:

  1. Load the decent browsers CSS
  2. Conditionally load IE6,7 overrides

The drawback is that IE6,7 get two HTTP requests. That's not good. Another drawback is that having a separate IE-overrides stylesheet is an excuse to get lazy and instead of solving a problem in a creative way, you (and the team) will just keep adding to it.

We can avoid the extra HTTP request by creating our CSS bundles on the server side and having two browser-specific but complete stylesheet files:

  1. The decent browsers CSS
  2. The complete CSS for IE6,7 not only the overrides

Then the question is loading one of the two conditionally without server-side UA sniffing. The trick (courtesy of duris.ru) is to use conditional comments to comment out the decent CSS so it's not loaded at all:

<!--[if lte IE 7]>
  <link href="IE67.css" rel="stylesheet" type="text/css" />
<![endif]-->
<!--[if gt IE 7]><!-->
  <link href="decent-browsers.css" rel="stylesheet" type="text/css" />
<!--<![endif]-->

The highlighting suggests what the decent browsers see.

IE6,7 see something like this after the conditional comments are processed:

  <link href="IE67.css" rel="stylesheet" type="text/css" />
<!--
  <link href="decent-browsers.css" rel="stylesheet" type="text/css" />
-->