Optimizing Perceived Performance

  1. Testing

  2. Optimizing Perceived Performance

  3. About Me

  4. My Startup

    “DBDB”

    http://dbdb.local/home
  5. The Problem

  6. The Problem (Con’t)

    def bagfactor
      sleep(2)
      rand(50) / 10.0
    end
    
  7. http://github.com/dce/dbdb

    • Rails app
    • public/slides.html
    • public/javacripts/jquery.jquinote.js
  8. It could happen to you

  9. First Impressions

  10. Up to... User response
    0.1 second instantaneous
    1 second responsive
    10 seconds slow
    > 10 seconds gone

    – About Face 3

    http://dbdb.local/
  11. Bring in the AJAX

  12. Progressive Enhancement

  13. Unobtrusive Javascript

  14. HIJAX

  15. jQuery

  16. class DbsController < ApplicationController
      def show
        @db = Db.find(params[:id])
        respond_to do |format|
          format.html
          format.js do
            render :partial => "profile",
              :locals => { :db => @db }
          end
        end
      end
    end
    
  17. $(".db-list a").one("click", function() {
      var link = $(this);
    
      $.ajax({
        url: this.href,
        success: function(src) {
          var bio = $("<dd>" + src + "</dd>");
    
          link.parents("dt").after(bio);
    
          link.click(function() {
            bio.toggle();
            return false;
          });
        }
      });
    
      return false;
    });
    
    http://dbdb.local/
  18. $(".db-list a").one("click", function() {
      var link = $(this);
      var bio  = $("<dd></dd>");
    
      bio.load(
        this.href + " dd > *", function() {
          link.parents("dt").after(bio);
    
          link.click(function() {
            bio.toggle();
            return false;
          });
    
          return true;
        }
      );
    
      return false;
    });
    
    http://dbdb.local/
  19. Make It Snappy

  20. $(".db-list a").one("click", function() {
      var link = $(this);
      var bio  = $('<dd class="spinner"></dd>');
    
      link.parents("dt").after(bio);
      bio.hide().slideDown("slow");
    
      link.click(function() {
        bio.slideToggle();
        return false;
      });
    
      $.ajax({
        url: this.href,
        success: function(src) {
          bio.html(src).removeClass("spinner");
        }
      });
    
      return false;
    });
    
    http://dbdb.local/
  21. Take Advantage of Downtime

  22. $(".db-list a").each(function() {
      var link = $(this);
      var bio  = $('<dd class="spinner"></dd>').hide();
    
      link.parents("dt").after(bio);
    
      link.click(function() {
        bio.slideToggle();
        return false;
      });
    });
    
    $.fn.loadContentInOrder = function() {
      return this.each(function() {
        var link = $(this);
    
        $.ajax({
          url: this.href,
          success: function(src) {
            link.parents("dt").next("dd").html(src)
              .removeClass("spinner")
              .next("dt").find("a").loadContentInOrder();
          }
        });
      });
    };
    
    $(".db-list a:first").loadContentInOrder();
    
    http://dbdb.local/
  23. Isolate Bottlenecks

  24. JSON

    {
      db: {
        id: 13,
        name: "Tyler Hansbrough",
        occupation: "UNC Basketball Player",
        bagfactor: 1.3,
        avatar_id: 61
      }
    }
    
  25. class DbsController < ApplicationController
      def show
        @db = Db.find(params[:id])
        respond_to do |format|
          format.html
          format.js { ... }
          format.json do
            render :json => { :bagfactor => @db.bagfactor }
          end
        end
      end
    end
    
  26. jQuery.fn.loadBagfactor = function() {

      var img = $(this);

    
      $.ajax({
        url: $("a", img.parents("dd").prev("dt")).attr("href"),

        data: { format: "json" },
        dataType: "json",

        success: function(db) {

          img.replaceWith(db.bagfactor);

          $("img.spinner:first").loadBagfactor();

        }

      });
    };

    
    $("img.spinner:first").loadBagfactor();
    
    http://dbdb.local/v4
  27. Worst Best Solution

    • Most information in least time
    • Poor degredation
    • Optimize for users' needs
  28. New Problem

  29. http://dbdb.local/dbs/new
  30. Remove Blocking Operations

  31. The Usual Way

    • Hidden iFrame
    • Second form w/ iFrame as target
    • Server sends back JS to update page
  32. Something Sorta Nuts

  33. avatars/new.html.erb

    <% form_for @avatar, :html => { :multipart => true } do |f| %>
      <%= f.file_field :image %>
    <% end %>
    
    <% javascript_tag do %>
      $("input").change(function() {
        $(this).hide().after('<%= image_tag "spinner.gif" %>');
        $(this).parents('form').submit();
      });
    <% end %>
    
  34. avatars/create.html.erb

    <%= image_tag @avatar.image.url(:thumb) %>
    
    <% javascript_tag do %>
      $("form", top.document).append(
        '<%= hidden_field_tag "db[avatar_id]", @avatar.id %>');
    <% end %>
    
  35. $("input[type=file]").replaceWith(
      '<iframe src="/avatars/new"></iframe>');
    
    http://dbdb.local/dbs/new
  36. Cool? Lame?

    (It doesn't matter)

  37. Server Side

  38. “At least 80 percent of the time it takes to display a web page happens after the HTML document has been downloaded.”

    — Steve Souders, High Performance Web Sites

  39. Fewer HTTP Requests

    • Combine scripts & CSS files
    • Use CSS sprites
    • Avoid redirects when possible
  40. Optimize Browser Caching

    • Use a far-future expires header
    • Put JS & CSS in external files
    • Apache mod_expires
  41. Reduce File Size

    • Minify JS - asset_packager
    • Use GZIP
    • Apache mod_deflate
  42. What's Next

    Rails 3

  43. The link_to_remote Helper

    <%= link_to_remote @db.name, :url => db_url(@db),
           :method => :get %>
    
  44. The Old Way

    # <%= link_to_remote @db.name, :url => db_url(@db),
    #       :method => :get %>
    
    <a href="#" onclick="$.ajax({data:'authenticity_token=' +
      encodeURIComponent('2b79b50423e099...'),
      dataType:'script', type:'get',
      url:'http://dbdb.local/dbs/13'}); return false;">
        Tyler Hansbrough
    </a>
    
  45. The New Way

    # <%= link_to_remote @db.name, :url => db_url(@db),
    #       :method => :get %>
    
    <a href="/dbs/13" data-remote="true" data-method="get">
      Tyler Hansbrough
    </a>
    
  46. The New Way (Con’t)

    # <%= link_to_remote @db.name, :url => db_url(@db),
    #       :method => :get %>
    
    # <a href="/dbs/13" data-remote="true" data-method="get">
    #   Tyler Hansbrough
    # </a>
    
    var request = function(options) {
      $.ajax($.extend({ url : options.url, type : 'get' },
        options));
      return false;
    };
    
    $('a[data-remote=true]').live('click', function() {
      return request({ url : this.href });
    });
    
    $('form[data-remote=true]').live('submit', function() {
      return request({
        url  : this.action,
        type : this.method,
        data : $(this).serialize()
      });
    });
    
  47.  # <a href="/dbs/13" class="remote">
     #   Tyler Hansbrough
     # </a>
    
     var request = function(options) {
       $.ajax($.extend({ url : options.url, type : 'get' },
         options));
       return false;
     };
    
     $("a.remote").click(function() {
       return request({ url : this.href });
     });
     
  48. Conclusion

  49. Consider perceived performance

  50. Write your own JS

  51. Optimize your server config

  52. Thank You

    http://speakerrate.com/talks/1450
    twitter: @deisinger

  53. Your Turn

    Stories? Questions?