Pressure is nothing more than the shadow of great opportunity. - Michael Johnson

Tips for Writing Nicer Site Badges

9 years, 4 months ago

Introduction

Yesterday Jeremy Zawodny wrote a post entitled JavaScript Badges and Widgets Considered Harmful. It follows on from Mike Arrington's post which talks about why TechCrunch went down recently, attributing part of the blame to poor performance of third party badges hosted on the site. Both posts have sparked interesting discussion about the problems with such badges and possible routes for improvement.

For me these posts are kind of timely. I've been investigating writing server side versions of such badges for exactly these and many other reasons. A little while ago I released a PHP version of the del.icio.us tagometer badge which implements caching to speed up rendering and reduce the chance of a slow connection to del.icio.us causing problems for any site which hosts it. You can see this in use on any page of my site and also on the YUI section of the Yahoo! Developer Network (thanks Eric). More on server side versions later - I'd like to start by running through my thoughts on writing cleaner (or improving current) JavaScript badges first.

Improving Front-end Code

Many badges have pretty nasty front-end code freely mixing inline styles with HTML. Writing lean semantic HTML and keeping CSS styling separate will ensure your badge is easier to customise to fit with users' existing site styles. Adding an option to suppress your CSS is the icing on the cake. At all costs avoid use of !important. Rules using this will always be applied over others referencing the same selectors regardless of specificity. In short, it will be almost impossible for users to override those styles.

Say no to document.write

This is seriously old school and forces your badge to be included in the HTML at the point at which you want it to appear. All browsers will block rendering of the rest of the page whilst downloading JavaScript files and are incredibly bad at handling slow or unresponsive scripts. In the worst case some will hang indefinitely. Only Opera provides decent script timeout functionality. It aborts unresponsive scripts after a few seconds and continues rendering the page but with several badges in use on one page even this may be small consolation.

Using DOM Methods

Instead consider using DOM methods to write your badge to the page appending your CSS to the site header via a dynamically created style element and your HTML to an existing element on the page using either innerHTML or the more long winded document.createElement. To keep things simple for less technical users I've experimented with inserting the badge before the script tag which references it and a more advanced option of supplying an arbitrary element ID into which the badge should be inserted for those users that want to position the badge script near the bottom of the page to mitigate potential performance or badge availability problems. By doing this you provide the best of both worlds.

The Simple Method
  1. <script type="text/javascript" id="script-id" src="http://www.ejeliot.com/script-url.php"></script>
The Advanced Method

Insert an empty container, such as a DIV, at the position you want your badge to appear in your page.

  1. <div id="badge-id"></div>

Then include the script at the bottom of your page, just before the closing body tag.

  1. <script type="text/javascript"> src="http://www.ejeliot.com/script-url.php?element=badge-id"> </script>
How To Implement

Simple PHP code can handle the presence (or lack) of an element reference serving up corresponding JavaScript to match. Here's an example of how you might handle such a badge.

  1. <?php
  2. header('Content-Type: text/javascript');
  3. if (isset($_GET['element']) && preg_match('/^[a-z0-9_-]+$/i', $_GET['element'])) {
  4. $sElement = $_GET['element'];
  5. } else {
  6. $sElement = 'script-id';
  7. }
  8. ?>
  9. var NAMESPACE = window.NAMESPACE || {}
  10. NAMESPACE.Badge = function() {
  11. var sCss = "<?php echo $sCss; ?>";
  12. var sHtml = "<?php echo $sHtml; ?>";
  13. function GetCss() {
  14. var oHead = document.getElementsByTagName('head')[0];
  15. var oCssStyle = document.createElement('style');
  16. oCssStyle.setAttribute('type', 'text/css');
  17. if (oCssStyle.styleSheet) {
  18. oCssStyle.styleSheet.cssText = sCss;
  19. } else {
  20. oCssStyle.appendChild(document.createTextNode(sCss));
  21. }
  22. oHead.appendChild(oCssStyle);
  23. };
  24. function GetHtml() {
  25. var oElement = document.getElementById('<?php echo $sElement; ?>');
  26. <?php if ($sElement == 'script-id') { ?>
  27. var oDiv = document.createElement('div');
  28. oElement.parentNode.insertBefore(oDiv, oElement).innerHTML = sHtml;
  29. <?php } else { ?>
  30. oElement.innerHTML = sHtml;
  31. <?php } ?>
  32. };
  33. return {
  34. Init : function() {
  35. if (document.getElementById && document.createTextNode) {
  36. GetCss();
  37. GetHtml();
  38. }
  39. }
  40. };
  41. }();
  42. NAMESPACE.Badge.Init();

Make sure to include the first line which sets a content type header to tell the browser it should expect JavaScript. If you omit this the JavaScript won't be executed in some browsers, most notably Opera.

The CSS needs to be appended separately to the header because Safari doesn't play nice with rules entered in the body, failing to render them. Of course one could argue that Safari is simply forcing you to do things the right way.

You'll also need to make sure the CSS and HTML returned in the PHP variables $sCss and $sHtml is reduced to one line to prevent errors in the resulting JavaScript. The following function should be suitable for this in most cases.

  1. function TextMin($sText) {
  2. $sText = str_replace('"', '\"', $sText);
  3. $sText = preg_replace('/\\s+/ ', ' ', $sText);
  4. return trim(str_replace('> <', '><', $sText));
  5. }

One HTTP Request for Everything

In the example code above you'll notice that the code which takes care of inserting the badge into the page also contains all it's data, HTML and CSS. Using PHP allows me to inject this into variables before returning the JavaScript and therefore eliminate the additional HTTP requests which would otherwise be required if I were pulling the data in via AJAX or dynamic creation of style/script nodes.

Server-Side Badges

As mentioned earlier my preferred method is to create server-side versions of your badges for those users where performance is critical. Doing so allows data requests to be cached which is kinder on your servers and provides the possibility of fallbacks should your service ever be unavailable.

I've written two classes (for HTTP requests and file caching) which allow for the following robustness mechanism.

Basic Steps
  1. Use the HTTP class to request badge data.
  2. If successful cache the returned data on disk for a defined period of time and subsequent page requests use that cached data until it expires.
  3. If unsuccessful look for expired cache data and re-validate to prevent immediate re-requests to the failing data URL.
  4. If unable to retrieve data from an expired cache, most likely because the data URL was unresponsive the first time the request was made, then don't display the badge.
Corresponding Code

The actual code might look like this.

  1. <?php
  2. define('DATA_URL', '');
  3. define('CACHE_TIME', 300); // 5 mins
  4. $bSuccessful = true;
  5. $oHttp = new Http();
  6. $oCache = new Cache(DATA_URL, CACHE_TIME);
  7. if (!$oCache->Check()) {
  8. if ($sData = $oHttp->Get(DATA_URL)) {
  9. $oCache->Set($sData);
  10. } elseif ($oCache->Exists()) {
  11. $oCache->ReValidate();
  12. } else {
  13. $bSuccessful = false;
  14. }
  15. }
  16. if ($bSuccessful) {
  17. $sData = $oCache->Get();
  18. ?>
  19. <!-- display badge here -->
  20. <?php } ?>
Other Benefits

Providing a server-side badge has other benefits - the contents of your badge will be crawl-able by search engines and your badge will be available with JavaScript disabled.

Feedback

This post is definitely work in progress and I'll be adding to it as and when I think of more. If you've got thoughts on the subject or think anything I've written above doesn't quite add up, post a comment below. I'd love you hear your take on the problem.

Comments

  • The 'defer' attribute on the script tag might help to prevent page lockup caused by problems loading javascript files from other sites.

    It tells the browser that the script is of low priority and to continue rendering the page instead of waiting to download it.

    Although it is a part of the HTML/XHTML standard it is not supported by Firefox.

    Stoyan Stefanov does a better job of explaining it over at phpied than I have so it might be worth checking his post for more information :)

    Aaron Bassett - 14th February 2007 #

  • Aaron - the defer attribute only provides a hint to the browser that it can defer rendering. It doesn't guarantee that it will. It's also only supported in IE as far as I'm aware - I ran a simple test which seems to confirm this. I didn't include it in the post above for these reasons.

    Ed Eliot - 15th February 2007 #

  • I didn't mean to give the impression that defer could solve all the problems associated with fetching external scripts, not by a long way!

    But I felt it was still relevant as it is part of the standard (although not widely supported) and if/when it gains better cross-browser support it could help to ease the problem.

    In the meantime it does not cause errors in unsupported browsers (they simply ignore it) so it won't do any harm to include it in the hope that it does gain support :)

    A method I have found to work well cross-browser is to create a simple js file which controls the attaching of all external scripts. This is added near the bottom of each page ensuring that all external scripts are not added to the DOM until at least the page has had a chance to render.

    Aaron Bassett - 15th February 2007 #

  • Aaron - you didn't at all and I really appreciate your input and links. I think my comment was badly phrased - apologies.

    Ed Eliot - 15th February 2007 #

  • Good thinking - 3rd-party JS has long been a problem because of the remote loading issue (ads are another common one.)

    Though it can be a bit tricky, I'd consider investigating dynamically creating and appending script elements using Javascript itself (createElement('script') and so on.) You can then create and append these elements to the body at parse time (or after onload), safely and without having to worry about remote hosts holding up your load time.

    Some gotchas are that the readyState in IE will report either 'loaded' or 'complete' (from script.onload) depending on Cache, and apparently there's a bug which prevents this from working 100% in current versions of Safari (see Scott Andrew's blog.) I still think it's perfectly doable, however. ;)

    Scott Schiller - 16th February 2007 #

  • Scott - thanks for the ideas, so you're thinking essentially a dynamic loader which sites could use to load in compatible badges?

    If script nodes are appended in place dynamically at parse time wouldn't they still block rendering?

    Ed Eliot - 18th February 2007 #

  • If you don't have control over external javascript, which contains document.write crap, you can also hide this in a invisible iframe and copy the DOM over to your page, once loaded.

    Some years ago I used this technique to speed up loading of a page with syndicated content.

    Later this technique became better known as the Iframe Transport method for Ajax.

    See: http://www.xs4all.nl/~jlpoutre/BoT/Javascript/async-write.html

    Joe - 22nd February 2007 #

  • Joe - interesting idea, thanks for the info and links.

    Ed Eliot - 24th February 2007 #

Help make this post better

Notes: Standard BBCode for links, bold, italic and code are supported. rel="nofollow" is added to all links. Your email address will never be displayed on the site.

Back to index