diff --git a/Nibriboard/ClientFiles/BoardWindow.js b/Nibriboard/ClientFiles/BoardWindow.js
index 24bb81b..9942b3f 100644
--- a/Nibriboard/ClientFiles/BoardWindow.js
+++ b/Nibriboard/ClientFiles/BoardWindow.js
@@ -14,6 +14,11 @@ class BoardWindow extends EventEmitter
 	{
 		super(); // Run the parent constructor
 		
+		this.maxFps = 60;
+		this.renderTimeIndicator = document.createElement("span");
+		this.renderTimeIndicator.innerHTML = "0ms";
+		document.querySelector(".fps").appendChild(this.renderTimeIndicator);
+		
 		this.canvas = canvas;
 		this.context = canvas.getContext("2d");
 		FaviconNotification.init({
@@ -38,8 +43,23 @@ class BoardWindow extends EventEmitter
 	
 	nextFrame()
 	{
-		this.update();
-		this.render(this.canvas, this.context);
+		// The time at which the current frame started rendering.
+		let frameStart = +new Date();
+		
+		if(frameStart - this.lastFrameStart >= (1 / this.maxFps) * 1000)
+		{
+			this.update();
+			this.render(this.canvas, this.context);
+		}
+		
+		// Update the time the last frame started rendering
+		this.lastFrameStart = frameStart;
+		// Update the time we took rendering the last frame
+		this.lastFrameTime = +new Date() - frameStart;
+		
+		this.renderTimeIndicator.innerHTML = `${this.lastFrameTime}ms`;
+		
+		// Limit the maximum fps
 		requestAnimationFrame(this.nextFrame.bind(this));
 	}
 	
diff --git a/Nibriboard/ClientFiles/Nibri.css b/Nibriboard/ClientFiles/Nibri.css
index 41c372c..a52782d 100644
--- a/Nibriboard/ClientFiles/Nibri.css
+++ b/Nibriboard/ClientFiles/Nibri.css
@@ -9,3 +9,8 @@ body
     position: absolute;
     top: 0; right: 0; bottom: 0; left: 0;
 }
+
+.fps
+{
+    top: 0.2em !important; right: 0.2em !important;
+}
diff --git a/Nibriboard/ClientFiles/NibriClient.js b/Nibriboard/ClientFiles/NibriClient.js
index d9d9b5c..cc7f22a 100644
--- a/Nibriboard/ClientFiles/NibriClient.js
+++ b/Nibriboard/ClientFiles/NibriClient.js
@@ -26,6 +26,11 @@ class BoardWindow extends EventEmitter
 	{
 		super(); // Run the parent constructor
 		
+		this.maxFps = 60;
+		this.renderTimeIndicator = document.createElement("span");
+		this.renderTimeIndicator.innerHTML = "0ms";
+		document.querySelector(".fps").appendChild(this.renderTimeIndicator);
+		
 		this.canvas = canvas;
 		this.context = canvas.getContext("2d");
 		FaviconNotification.init({
@@ -50,8 +55,23 @@ class BoardWindow extends EventEmitter
 	
 	nextFrame()
 	{
-		this.update();
-		this.render(this.canvas, this.context);
+		// The time at which the current frame started rendering.
+		let frameStart = +new Date();
+		
+		if(frameStart - this.lastFrameStart >= (1 / this.maxFps) * 1000)
+		{
+			this.update();
+			this.render(this.canvas, this.context);
+		}
+		
+		// Update the time the last frame started rendering
+		this.lastFrameStart = frameStart;
+		// Update the time we took rendering the last frame
+		this.lastFrameTime = +new Date() - frameStart;
+		
+		this.renderTimeIndicator.innerHTML = `${this.lastFrameTime}ms`;
+		
+		// Limit the maximum fps
 		requestAnimationFrame(this.nextFrame.bind(this));
 	}
 	
@@ -87,14 +107,22 @@ class BoardWindow extends EventEmitter
 	}
 }
 
+window.FpsIndicator = require("fps-indicator");
+
 window.addEventListener("load", function (event) {
-	var canvas = document.getElementById("canvas-main"),
+	let fpsIndicator = FpsIndicator({
+		updatePeriod: 1000,
+		maxFps: 60
+	});
+	fpsIndicator.element.style.color = "rgb(114, 194, 179)";
+	
+	let canvas = document.getElementById("canvas-main"),
 		boardWindow = new BoardWindow(canvas);
 	boardWindow.nextFrame();
 	window.boardWindow = boardWindow;
 });
 
-},{"event-emitter-es6":2,"favicon-notification":3}],2:[function(require,module,exports){
+},{"event-emitter-es6":2,"favicon-notification":3,"fps-indicator":4}],2:[function(require,module,exports){
 'use strict';
 
 var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
@@ -485,4 +513,419 @@ module.exports = EventEmitter;
 
 }));
 
+},{}],4:[function(require,module,exports){
+/**
+ * @module fps-indicator
+ */
+
+const raf = require('raf');
+const now = require('right-now');
+
+module.exports = fps;
+
+
+
+function fps (opts) {
+	if (!(this instanceof fps)) return new fps(opts);
+
+	opts = opts || {};
+
+	if (opts.container) {
+		if (typeof opts.container === 'string') {
+			this.container = document.querySelector(opts.container);
+		}
+		else {
+			this.container = opts.container;
+		}
+	}
+	else {
+		this.container = document.body || document.documentElement;
+	}
+
+	//init fps
+	this.element = document.createElement('div');
+	this.element.classList.add('fps');
+	this.element.innerHTML = `
+		<div class="fps-bg"></div>
+		<canvas class="fps-canvas"></canvas>
+		<span class="fps-text">fps <span class="fps-value">60.0</span></span>
+	`;
+	this.container.appendChild(this.element);
+
+	this.canvas = this.element.querySelector('.fps-canvas');
+	this.textEl = this.element.querySelector('.fps-text');
+	this.valueEl = this.element.querySelector('.fps-value');
+	this.bgEl = this.element.querySelector('.fps-bg');
+
+	this.element.style.cssText = `
+		line-height: 1;
+		position: absolute;
+		z-index: 1;
+		top: 0;
+		right: 0;
+	`;
+
+	this.canvas.style.cssText = `
+		position: relative;
+		width: 2em;
+		height: 1em;
+		display: block;
+		float: left;
+		margin-right: .333em;
+	`;
+
+	this.bgEl.style.cssText = `
+		position: absolute;
+		height: 1em;
+		width: 2em;
+		background: currentcolor;
+		opacity: .1;
+	`;
+
+	this.canvas.width = parseInt(getComputedStyle(this.canvas).width) || 1;
+	this.canvas.height = parseInt(getComputedStyle(this.canvas).height) || 1;
+
+	this.context = this.canvas.getContext('2d');
+
+	let ctx = this.context;
+	let w = this.canvas.width;
+	let h = this.canvas.height;
+	let count = 0;
+	let lastTime = 0;
+	let values = opts.values || Array(this.canvas.width);
+	let updatePeriod = opts.updatePeriod || 1000;
+	let maxFps = opts.maxFps || 100;
+
+	//enable update routine
+	let that = this;
+	raf(function measure () {
+		count++;
+		let t = now();
+
+		if (t - lastTime > updatePeriod) {
+			let color = that.color;
+			lastTime = t;
+			values.push(count / (maxFps * updatePeriod * 0.001));
+			values = values.slice(-w);
+			count = 0;
+
+			ctx.clearRect(0, 0, w, h);
+			ctx.fillStyle = getComputedStyle(that.canvas).color;
+			for (let i = w; i--;) {
+				let value = values[i];
+				if (value == null) break;
+				ctx.fillRect(i, h - h * value, 1, h * value);
+			}
+
+			that.valueEl.innerHTML = (values[values.length - 1]*maxFps).toFixed(1);
+		}
+
+		raf(measure);
+	});
+}
+},{"raf":6,"right-now":7}],5:[function(require,module,exports){
+(function (process){
+// Generated by CoffeeScript 1.7.1
+(function() {
+  var getNanoSeconds, hrtime, loadTime;
+
+  if ((typeof performance !== "undefined" && performance !== null) && performance.now) {
+    module.exports = function() {
+      return performance.now();
+    };
+  } else if ((typeof process !== "undefined" && process !== null) && process.hrtime) {
+    module.exports = function() {
+      return (getNanoSeconds() - loadTime) / 1e6;
+    };
+    hrtime = process.hrtime;
+    getNanoSeconds = function() {
+      var hr;
+      hr = hrtime();
+      return hr[0] * 1e9 + hr[1];
+    };
+    loadTime = getNanoSeconds();
+  } else if (Date.now) {
+    module.exports = function() {
+      return Date.now() - loadTime;
+    };
+    loadTime = Date.now();
+  } else {
+    module.exports = function() {
+      return new Date().getTime() - loadTime;
+    };
+    loadTime = new Date().getTime();
+  }
+
+}).call(this);
+
+}).call(this,require('_process'))
+},{"_process":8}],6:[function(require,module,exports){
+(function (global){
+var now = require('performance-now')
+  , root = typeof window === 'undefined' ? global : window
+  , vendors = ['moz', 'webkit']
+  , suffix = 'AnimationFrame'
+  , raf = root['request' + suffix]
+  , caf = root['cancel' + suffix] || root['cancelRequest' + suffix]
+
+for(var i = 0; !raf && i < vendors.length; i++) {
+  raf = root[vendors[i] + 'Request' + suffix]
+  caf = root[vendors[i] + 'Cancel' + suffix]
+      || root[vendors[i] + 'CancelRequest' + suffix]
+}
+
+// Some versions of FF have rAF but not cAF
+if(!raf || !caf) {
+  var last = 0
+    , id = 0
+    , queue = []
+    , frameDuration = 1000 / 60
+
+  raf = function(callback) {
+    if(queue.length === 0) {
+      var _now = now()
+        , next = Math.max(0, frameDuration - (_now - last))
+      last = next + _now
+      setTimeout(function() {
+        var cp = queue.slice(0)
+        // Clear queue here to prevent
+        // callbacks from appending listeners
+        // to the current frame's queue
+        queue.length = 0
+        for(var i = 0; i < cp.length; i++) {
+          if(!cp[i].cancelled) {
+            try{
+              cp[i].callback(last)
+            } catch(e) {
+              setTimeout(function() { throw e }, 0)
+            }
+          }
+        }
+      }, Math.round(next))
+    }
+    queue.push({
+      handle: ++id,
+      callback: callback,
+      cancelled: false
+    })
+    return id
+  }
+
+  caf = function(handle) {
+    for(var i = 0; i < queue.length; i++) {
+      if(queue[i].handle === handle) {
+        queue[i].cancelled = true
+      }
+    }
+  }
+}
+
+module.exports = function(fn) {
+  // Wrap in a new function to prevent
+  // `cancel` potentially being assigned
+  // to the native rAF function
+  return raf.call(root, fn)
+}
+module.exports.cancel = function() {
+  caf.apply(root, arguments)
+}
+module.exports.polyfill = function() {
+  root.requestAnimationFrame = raf
+  root.cancelAnimationFrame = caf
+}
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"performance-now":5}],7:[function(require,module,exports){
+(function (global){
+module.exports =
+  global.performance &&
+  global.performance.now ? function now() {
+    return performance.now()
+  } : Date.now || function now() {
+    return +new Date
+  }
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],8:[function(require,module,exports){
+// shim for using process in browser
+var process = module.exports = {};
+
+// cached from whatever global is present so that test runners that stub it
+// don't break things.  But we need to wrap it in a try catch in case it is
+// wrapped in strict mode code which doesn't define any globals.  It's inside a
+// function because try/catches deoptimize in certain engines.
+
+var cachedSetTimeout;
+var cachedClearTimeout;
+
+function defaultSetTimout() {
+    throw new Error('setTimeout has not been defined');
+}
+function defaultClearTimeout () {
+    throw new Error('clearTimeout has not been defined');
+}
+(function () {
+    try {
+        if (typeof setTimeout === 'function') {
+            cachedSetTimeout = setTimeout;
+        } else {
+            cachedSetTimeout = defaultSetTimout;
+        }
+    } catch (e) {
+        cachedSetTimeout = defaultSetTimout;
+    }
+    try {
+        if (typeof clearTimeout === 'function') {
+            cachedClearTimeout = clearTimeout;
+        } else {
+            cachedClearTimeout = defaultClearTimeout;
+        }
+    } catch (e) {
+        cachedClearTimeout = defaultClearTimeout;
+    }
+} ())
+function runTimeout(fun) {
+    if (cachedSetTimeout === setTimeout) {
+        //normal enviroments in sane situations
+        return setTimeout(fun, 0);
+    }
+    // if setTimeout wasn't available but was latter defined
+    if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
+        cachedSetTimeout = setTimeout;
+        return setTimeout(fun, 0);
+    }
+    try {
+        // when when somebody has screwed with setTimeout but no I.E. maddness
+        return cachedSetTimeout(fun, 0);
+    } catch(e){
+        try {
+            // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
+            return cachedSetTimeout.call(null, fun, 0);
+        } catch(e){
+            // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
+            return cachedSetTimeout.call(this, fun, 0);
+        }
+    }
+
+
+}
+function runClearTimeout(marker) {
+    if (cachedClearTimeout === clearTimeout) {
+        //normal enviroments in sane situations
+        return clearTimeout(marker);
+    }
+    // if clearTimeout wasn't available but was latter defined
+    if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
+        cachedClearTimeout = clearTimeout;
+        return clearTimeout(marker);
+    }
+    try {
+        // when when somebody has screwed with setTimeout but no I.E. maddness
+        return cachedClearTimeout(marker);
+    } catch (e){
+        try {
+            // When we are in I.E. but the script has been evaled so I.E. doesn't  trust the global object when called normally
+            return cachedClearTimeout.call(null, marker);
+        } catch (e){
+            // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
+            // Some versions of I.E. have different rules for clearTimeout vs setTimeout
+            return cachedClearTimeout.call(this, marker);
+        }
+    }
+
+
+
+}
+var queue = [];
+var draining = false;
+var currentQueue;
+var queueIndex = -1;
+
+function cleanUpNextTick() {
+    if (!draining || !currentQueue) {
+        return;
+    }
+    draining = false;
+    if (currentQueue.length) {
+        queue = currentQueue.concat(queue);
+    } else {
+        queueIndex = -1;
+    }
+    if (queue.length) {
+        drainQueue();
+    }
+}
+
+function drainQueue() {
+    if (draining) {
+        return;
+    }
+    var timeout = runTimeout(cleanUpNextTick);
+    draining = true;
+
+    var len = queue.length;
+    while(len) {
+        currentQueue = queue;
+        queue = [];
+        while (++queueIndex < len) {
+            if (currentQueue) {
+                currentQueue[queueIndex].run();
+            }
+        }
+        queueIndex = -1;
+        len = queue.length;
+    }
+    currentQueue = null;
+    draining = false;
+    runClearTimeout(timeout);
+}
+
+process.nextTick = function (fun) {
+    var args = new Array(arguments.length - 1);
+    if (arguments.length > 1) {
+        for (var i = 1; i < arguments.length; i++) {
+            args[i - 1] = arguments[i];
+        }
+    }
+    queue.push(new Item(fun, args));
+    if (queue.length === 1 && !draining) {
+        runTimeout(drainQueue);
+    }
+};
+
+// v8 likes predictible objects
+function Item(fun, array) {
+    this.fun = fun;
+    this.array = array;
+}
+Item.prototype.run = function () {
+    this.fun.apply(null, this.array);
+};
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+process.version = ''; // empty string to avoid regexp issues
+process.versions = {};
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+
+process.binding = function (name) {
+    throw new Error('process.binding is not supported');
+};
+
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+    throw new Error('process.chdir is not supported');
+};
+process.umask = function() { return 0; };
+
 },{}]},{},[1]);
diff --git a/Nibriboard/ClientFiles/index.js b/Nibriboard/ClientFiles/index.js
index 5b700f2..dcf17df 100644
--- a/Nibriboard/ClientFiles/index.js
+++ b/Nibriboard/ClientFiles/index.js
@@ -1,9 +1,17 @@
 "use strict";
 
+window.FpsIndicator = require("fps-indicator");
+
 import BoardWindow from './BoardWindow';
 
 window.addEventListener("load", function (event) {
-	var canvas = document.getElementById("canvas-main"),
+	let fpsIndicator = FpsIndicator({
+		updatePeriod: 1000,
+		maxFps: 60
+	});
+	fpsIndicator.element.style.color = "rgb(114, 194, 179)";
+	
+	let canvas = document.getElementById("canvas-main"),
 		boardWindow = new BoardWindow(canvas);
 	boardWindow.nextFrame();
 	window.boardWindow = boardWindow;
diff --git a/Nibriboard/ClientFiles/package.json b/Nibriboard/ClientFiles/package.json
index c5097c6..a525bd6 100644
--- a/Nibriboard/ClientFiles/package.json
+++ b/Nibriboard/ClientFiles/package.json
@@ -2,10 +2,12 @@
   "name": "nibriboard-client",
   "version": "0.0.1",
   "description": "The client program for nibriboard, an infinite whiteboard.",
+  "homepage": "https://git.starbeamrainbowlabs.com/sbrl/Nibriboard#nibriboard",
   "main": "index.js",
   "dependencies": {
     "rollupify": "^0.3.8"
   },
+  
   "devDependencies": {
     "rollupify": "^0.3.8"
   },
diff --git a/README.md b/README.md
index abe56a4..1a20a9b 100644
--- a/README.md
+++ b/README.md
@@ -10,5 +10,5 @@ An infinite whiteboard for recording those big ideas.
  - Future reference: Libraries I am considering
 	 - [Paper.js](http://paperjs.org/) - Client-side rendering
 	 - [pan-zoom](https://www.npmjs.com/package/pan-zoom)
-	 - [fps-indicator](https://www.npmjs.com/package/fps-indicator)
+	 - [fps-indicator](https://www.npmjs.com/package/fps-indicator) - The small FPS graph in the top corner
  - [IotWeb](http://sensaura.org/pages/tools/iotweb/) - Underlying HTTP / WebSocket server
\ No newline at end of file
