diff --git a/src/canvas.js b/src/canvas.js
index e056fe0f2..cd49c88b1 100644
--- a/src/canvas.js
+++ b/src/canvas.js
@@ -257,9 +257,6 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
       this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height);
       this.textDivs = [];
       this.textLayerQueue = [];
-      // Prevent textLayerQueue from being rendered while rendering a new page
-      if (this.textLayerTimer)
-        clearTimeout(this.textLayerTimer);
     },
 
     executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR,
@@ -328,31 +325,22 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
         return;
 
       var self = this;
-      var renderTextLayer = function canvasRenderTextLayer() {
-        var textDivs = self.textDivs;
-        for (var i = 0, length = textDivs.length; i < length; ++i) {
-          if (textDivs[i].dataset.textLength > 1) { // avoid div by zero
-            textLayer.appendChild(textDivs[i]);
-            // Adjust div width (via letterSpacing) to match canvas text
-            // Due to the .offsetWidth calls, this is slow
-            textDivs[i].style.letterSpacing =
-              ((textDivs[i].dataset.canvasWidth - textDivs[i].offsetWidth) /
-               (textDivs[i].dataset.textLength - 1)) + 'px';
-          }
+      var textDivs = this.textDivs;
+      this.textLayerTimer = setInterval(function renderTextLayer() {
+        if (textDivs.length === 0) {
+          clearInterval(self.textLayerTimer);
+          return;
         }
-      }
-      var textLayerQueue = this.textLayerQueue;
-      textLayerQueue.push(renderTextLayer);
-
-      // Lazy textLayer rendering (to prevent UI hangs)
-      // Only render queue if activity has stopped, where "no activity" ==
-      // "no beginDrawing() calls in the last N ms"
-      this.textLayerTimer = setTimeout(function renderTextLayerQueue() {
-        // Render most recent (==most relevant) layers first
-        for (var i = textLayerQueue.length - 1; i >= 0; i--) {
-          textLayerQueue.pop().call();
+        var textDiv = textDivs.shift();
+        if (textDiv.dataset.textLength > 1) { // avoid div by zero
+          textLayer.appendChild(textDiv);
+          // Adjust div width (via letterSpacing) to match canvas text
+          // Due to the .offsetWidth calls, this is slow
+          textDiv.style.letterSpacing =
+            ((textDiv.dataset.canvasWidth - textDiv.offsetWidth) /
+              (textDiv.dataset.textLength - 1)) + 'px';
         }
-      }, 500);
+      }, 0);
     },
 
     // Graphics state
@@ -1106,9 +1094,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
 
     paintImageXObject: function canvasGraphicsPaintImageXObject(objId) {
       var imgData = this.objs.get(objId);
-      if (!imgData) {
+      if (!imgData)
         error('Dependent image isn\'t ready yet');
-      }
+
       this.save();
       var ctx = this.ctx;
       var w = imgData.width;
diff --git a/src/colorspace.js b/src/colorspace.js
index 231ff6923..8088ab7df 100644
--- a/src/colorspace.js
+++ b/src/colorspace.js
@@ -154,6 +154,29 @@ var ColorSpace = (function ColorSpaceClosure() {
     }
     return null;
   };
+  /**
+   * Checks if a decode map matches the default decode map for a color space.
+   * This handles the general decode maps where there are two values per
+   * component. e.g. [0, 1, 0, 1, 0, 1] for a RGB color.
+   * This does not handle Lab, Indexed, or Pattern decode maps since they are
+   * slightly different.
+   * @param {Array} decode Decode map (usually from an image).
+   * @param {Number} n Number of components the color space has.
+   */
+  ColorSpace.isDefaultDecode = function colorSpaceIsDefaultDecode(decode, n) {
+    if (!decode)
+      return true;
+
+    if (n * 2 !== decode.length) {
+      warning('The decode map is not the correct length');
+      return true;
+    }
+    for (var i = 0, ii = decode.length; i < ii; i += 2) {
+      if (decode[i] != 0 || decode[i + 1] != 1)
+        return false;
+    }
+    return true;
+  };
 
   return ColorSpace;
 })();
@@ -200,6 +223,9 @@ var AlternateCS = (function AlternateCSClosure() {
           baseBuf[pos++] = 255 * tinted[j];
       }
       return base.getRgbBuffer(baseBuf, 8);
+    },
+    isDefaultDecode: function altcs_isDefaultDecode(decodeMap) {
+      ColorSpace.isDefaultDecode(decodeMap, this.numComps);
     }
   };
 
@@ -267,6 +293,10 @@ var IndexedCS = (function IndexedCSClosure() {
       }
 
       return base.getRgbBuffer(baseBuf, 8);
+    },
+    isDefaultDecode: function indexcs_isDefaultDecode(decodeMap) {
+      // indexed color maps shouldn't be changed
+      return true;
     }
   };
   return IndexedCS;
@@ -295,6 +325,9 @@ var DeviceGrayCS = (function DeviceGrayCSClosure() {
         rgbBuf[j++] = c;
       }
       return rgbBuf;
+    },
+    isDefaultDecode: function graycs_isDefaultDecode(decodeMap) {
+      ColorSpace.isDefaultDecode(decodeMap, this.numComps);
     }
   };
   return DeviceGrayCS;
@@ -319,6 +352,9 @@ var DeviceRgbCS = (function DeviceRgbCSClosure() {
       for (i = 0; i < length; ++i)
         rgbBuf[i] = (scale * input[i]) | 0;
       return rgbBuf;
+    },
+    isDefaultDecode: function rgbcs_isDefaultDecode(decodeMap) {
+      ColorSpace.isDefaultDecode(decodeMap, this.numComps);
     }
   };
   return DeviceRgbCS;
@@ -403,6 +439,9 @@ var DeviceCmykCS = (function DeviceCmykCSClosure() {
       }
 
       return rgbBuf;
+    },
+    isDefaultDecode: function cmykcs_isDefaultDecode(decodeMap) {
+      ColorSpace.isDefaultDecode(decodeMap, this.numComps);
     }
   };
 
diff --git a/src/evaluator.js b/src/evaluator.js
index 04556c717..213fe2178 100644
--- a/src/evaluator.js
+++ b/src/evaluator.js
@@ -227,7 +227,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
               data: new Uint8Array(w * h * 4)
             };
             var pixels = imgData.data;
-            imageObj.fillRgbaBuffer(pixels, imageObj.decode);
+            imageObj.fillRgbaBuffer(pixels);
             handler.send('obj', [objId, 'Image', imgData]);
           }, handler, xref, resources, image, inline);
       }
diff --git a/src/image.js b/src/image.js
index 987542c58..22a9f9eb4 100644
--- a/src/image.js
+++ b/src/image.js
@@ -24,6 +24,15 @@ var PDFImage = (function PDFImageClosure() {
       promise.resolve(image);
     }
   }
+  /**
+   * Decode and clamp a value. The formula is different from the spec because we
+   * don't decode to float range [0,1], we decode it in the [0,max] range.
+   */
+  function decodeAndClamp(value, addend, coefficient, max) {
+    value = addend + value * coefficient;
+    // Clamp the value to the range
+    return value < 0 ? 0 : value > max ? max : value;
+  }
   function PDFImage(xref, res, image, inline, smask) {
     this.image = image;
     if (image.getParams) {
@@ -69,6 +78,21 @@ var PDFImage = (function PDFImageClosure() {
     }
 
     this.decode = dict.get('Decode', 'D');
+    this.needsDecode = false;
+    if (this.decode && this.colorSpace &&
+        !this.colorSpace.isDefaultDecode(this.decode)) {
+      this.needsDecode = true;
+      // Do some preprocessing to avoid more math.
+      var max = (1 << bitsPerComponent) - 1;
+      this.decodeCoefficients = [];
+      this.decodeAddends = [];
+      for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) {
+        var dmin = this.decode[i];
+        var dmax = this.decode[i + 1];
+        this.decodeCoefficients[j] = dmax - dmin;
+        this.decodeAddends[j] = max * dmin;
+      }
+    }
 
     var mask = xref.fetchIfRef(dict.get('Mask'));
 
@@ -104,22 +128,43 @@ var PDFImage = (function PDFImageClosure() {
   };
 
   PDFImage.prototype = {
-    getComponents: function getComponents(buffer, decodeMap) {
+    getComponents: function getComponents(buffer) {
       var bpc = this.bpc;
-      if (bpc == 8)
+      var needsDecode = this.needsDecode;
+      var decodeMap = this.decode;
+
+      // This image doesn't require any extra work.
+      if (bpc == 8 && !needsDecode)
         return buffer;
 
+      var bufferLength = buffer.length;
       var width = this.width;
       var height = this.height;
       var numComps = this.numComps;
 
-      var length = width * height;
+      var length = width * height * numComps;
       var bufferPos = 0;
       var output = bpc <= 8 ? new Uint8Array(length) :
         bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length);
       var rowComps = width * numComps;
+      var decodeAddends, decodeCoefficients;
+      if (needsDecode) {
+        decodeAddends = this.decodeAddends;
+        decodeCoefficients = this.decodeCoefficients;
+      }
+      var max = (1 << bpc) - 1;
 
-      if (bpc == 1) {
+      if (bpc == 8) {
+        // Optimization for reading 8 bpc images that have a decode.
+        for (var i = 0, ii = length; i < ii; ++i) {
+          var compIndex = i % numComps;
+          var value = buffer[i];
+          value = decodeAndClamp(value, decodeAddends[compIndex],
+                          decodeCoefficients[compIndex], max);
+          output[i] = value;
+        }
+      } else if (bpc == 1) {
+        // Optimization for reading 1 bpc images.
         var valueZero = 0, valueOne = 1;
         if (decodeMap) {
           valueZero = decodeMap[0] ? 1 : 0;
@@ -144,8 +189,7 @@ var PDFImage = (function PDFImageClosure() {
           output[i] = !(buf & mask) ? valueZero : valueOne;
         }
       } else {
-        if (decodeMap != null)
-          TODO('interpolate component values');
+        // The general case that handles all other bpc values.
         var bits = 0, buf = 0;
         for (var i = 0, ii = length; i < ii; ++i) {
           if (i % rowComps == 0) {
@@ -159,7 +203,13 @@ var PDFImage = (function PDFImageClosure() {
           }
 
           var remainingBits = bits - bpc;
-          output[i] = buf >> remainingBits;
+          var value = buf >> remainingBits;
+          if (needsDecode) {
+            var compIndex = i % numComps;
+            value = decodeAndClamp(value, decodeAddends[compIndex],
+                            decodeCoefficients[compIndex], max);
+          }
+          output[i] = value;
           buf = buf & ((1 << remainingBits) - 1);
           bits = remainingBits;
         }
@@ -210,7 +260,7 @@ var PDFImage = (function PDFImageClosure() {
         }
       }
     },
-    fillRgbaBuffer: function fillRgbaBuffer(buffer, decodeMap) {
+    fillRgbaBuffer: function fillRgbaBuffer(buffer) {
       var numComps = this.numComps;
       var width = this.width;
       var height = this.height;
@@ -221,7 +271,7 @@ var PDFImage = (function PDFImageClosure() {
       var imgArray = this.getImageBytes(height * rowBytes);
 
       var comps = this.colorSpace.getRgbBuffer(
-        this.getComponents(imgArray, decodeMap), bpc);
+        this.getComponents(imgArray), bpc);
       var compsPos = 0;
       var opacity = this.getOpacity();
       var opacityPos = 0;
diff --git a/src/obj.js b/src/obj.js
index 2f7488a76..453014a91 100644
--- a/src/obj.js
+++ b/src/obj.js
@@ -22,6 +22,17 @@ var Cmd = (function CmdClosure() {
   Cmd.prototype = {
   };
 
+
+  var cmdCache = {};
+
+  Cmd.get = function cmdGet(cmd) {
+    var cmdValue = cmdCache[cmd];
+    if (cmdValue)
+      return cmdValue;
+
+    return cmdCache[cmd] = new Cmd(cmd);
+  };
+
   return Cmd;
 })();
 
diff --git a/src/parser.js b/src/parser.js
index 695438379..e50b12b9b 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -157,7 +157,7 @@ var Parser = (function ParserClosure() {
       imageStream = this.filter(imageStream, dict, length);
       imageStream.parameters = dict;
 
-      this.buf2 = new Cmd('EI');
+      this.buf2 = Cmd.get('EI');
       this.shift();
 
       return imageStream;
@@ -496,14 +496,14 @@ var Lexer = (function LexerClosure() {
         // array punctuation
         case '[':
         case ']':
-          return new Cmd(ch);
+          return Cmd.get(ch);
         // hex string or dict punctuation
         case '<':
           ch = stream.lookChar();
           if (ch == '<') {
             // dict punctuation
             stream.skip();
-            return new Cmd('<<');
+            return Cmd.get('<<');
           }
           return this.getHexString(ch);
         // dict punctuation
@@ -511,11 +511,11 @@ var Lexer = (function LexerClosure() {
           ch = stream.lookChar();
           if (ch == '>') {
             stream.skip();
-            return new Cmd('>>');
+            return Cmd.get('>>');
           }
         case '{':
         case '}':
-          return new Cmd(ch);
+          return Cmd.get(ch);
         // fall through
         case ')':
           error('Illegal character: ' + ch);
@@ -538,7 +538,7 @@ var Lexer = (function LexerClosure() {
         return false;
       if (str == 'null')
         return null;
-      return new Cmd(str);
+      return Cmd.get(str);
     },
     skipToNextLine: function lexerSkipToNextLine() {
       var stream = this.stream;
diff --git a/web/compatibility.js b/web/compatibility.js
index 7d1d72553..e4e2f2440 100644
--- a/web/compatibility.js
+++ b/web/compatibility.js
@@ -205,3 +205,15 @@
   });
 })();
 
+// HTMLElement dataset property
+(function checkDatasetProperty() {
+  var div = document.createElement('div');
+  if ('dataset' in div)
+    return; // dataset property exists
+  Object.defineProperty(HTMLElement.prototype, 'dataset', {
+    get: function htmlElementDatasetGetter() {
+      // adding dataset field to the actual object
+      return (this.dataset = {});
+    }
+  });
+})();
diff --git a/web/viewer.js b/web/viewer.js
index e75eb2c32..79df8280d 100644
--- a/web/viewer.js
+++ b/web/viewer.js
@@ -970,18 +970,38 @@ window.addEventListener('keydown', function keydown(evt) {
       return; // ignoring if the 'controls' element is focused
     curElement = curElement.parentNode;
   }
+  var handled = false;
   switch (evt.keyCode) {
     case 61: // FF/Mac '='
     case 107: // FF '+' and '='
     case 187: // Chrome '+'
       PDFView.zoomIn();
+      handled = true;
       break;
     case 109: // FF '-'
     case 189: // Chrome '-'
       PDFView.zoomOut();
+      handled = true;
       break;
     case 48: // '0'
       PDFView.setScale(kDefaultScale, true);
+      handled = true;
       break;
+    case 37: // left arrow
+    case 75: // 'k'
+    case 80: // 'p'
+      PDFView.page--;
+      handled = true;
+      break;
+    case 39: // right arrow
+    case 74: // 'j'
+    case 78: // 'n'
+      PDFView.page++;
+      handled = true;
+      break;
+  }
+
+  if (handled) {
+    evt.preventDefault();
   }
 });