blob: 639d6b96a7ccab412d1acda475d9ef8735b305f3 [file] [log] [blame]
// Copyright 2011 Google Inc. All Rights Reserved.
/**
* @fileoverview Client side logic for loading panels.
* TODO(gagansingh): Add TTI, CSI, benchmark, Tests.
* @author gagansingh@google.com (Gagan Singh)
*/
// States.
var CRITICAL_DATA_LOADED = 'cdl';
var NON_CRITICAL_LOADED = 'ncl';
// Exporting functions using quoted attributes to prevent js compiler from
// renaming them.
// See http://code.google.com/closure/compiler/docs/api-tutorial3.html#dangers
window['pagespeed'] = window['pagespeed'] || {};
var pagespeed = window['pagespeed'];
/**
* Class for loading panels.
*
* @constructor
*/
pagespeed.PanelLoader = function() {
this.readyToLoadNonCritical = false;
this.delayedNonCriticalData = null;
this.nonCriticalData = {};
this.nonCriticalDataPresent = false;
this.nonCacheablePanelInstances = {};
this.pageManager = new PageManager();
this.dashboardDisplayTime = 0;
this.csiTimings = {time: {}, size: {}};
this.contentSizeKb = 0;
this.debugIp = false;
// Using navigation timing api for start time. This api is available in
// Internet Explorer 9, Google Chrome 6 and Firefox 7.
// If not present, we trigger the start time when the rendering started.
if (window.performance) {
// timeStart will be number of milliseconds
this.timeStart = window.performance.timing.navigationStart;
} else {
this.timeStart = window.mod_pagespeed_start;
}
this.changePageLoadState(CRITICAL_DATA_LOADED, 0);
};
/**
* Main state machine for loading panels.
* NOTE: This function assumes the following order of calls:
* Critical data --> Critical images --> Callback for critical low res
* --> Callback for critical hi res --> Non critical data
*/
pagespeed.PanelLoader.prototype.loadData = function() {
if (this.nonCriticalDataPresent && this.readyToLoadNonCritical &&
this.state != NON_CRITICAL_LOADED) {
this.pageManager.instantiatePage(this.nonCriticalData);
// Remove 'DONT_BIND' in all non-cacheable objects.
for (var panelId in this.nonCacheablePanelInstances) {
if (!this.nonCacheablePanelInstances.hasOwnProperty(panelId)) continue;
var panelData = this.nonCacheablePanelInstances[panelId];
for (var i = 0; i < panelData.length; ++i) {
panelData[i][DONT_BIND] = false;
}
}
// Load Non-Critical Non-Cacheable Panels.
this.pageManager.instantiatePage(this.nonCacheablePanelInstances);
this.changePageLoadState(NON_CRITICAL_LOADED);
// Scroll to the hash fragment when it is given, as it can be found
// in Non-Critical panels loaded just now.
var hash = window.location.hash;
if (hash && hash[0] == '#') {
// Make sure the hash fragment refers to an element, since it can
// be "parameters" of Ajax applications.
if (document.getElementById(hash.slice(1))) {
// Use window.location.replace() here rather than a more popular
// technique <element>.scrollIntoView() to let browsers track on
// the element even if it gets moved/resized later on by scripts,
// such as deferJs and window.setTimeout().
window.location.replace(hash);
}
}
if (window.pagespeed && window.pagespeed.deferJs) {
window.pagespeed.deferJs.registerScriptTags();
var scriptExecutor = function() {
window.pagespeed.deferJs.run();
};
// NOTE: setTimeout can cause DOMContentLoaded to be fired before scripts
// start executing, causing problems with JQuery. Not much can be done
// because delaying <script src> have similar side effects, hence just
// making a note of the issue.
setTimeout(scriptExecutor, 1);
}
return;
}
};
pagespeed.PanelLoader.prototype['loadData'] =
pagespeed.PanelLoader.prototype.loadData;
/**
* Determines whether the given state means that client is still rendering
* critical portions.
* @param {string} state
* @return {boolean}
*/
pagespeed.PanelLoader.prototype.isStateInCriticalPath = function(state) {
return state == CRITICAL_DATA_LOADED;
};
/**
* Accessor for csi timings as a string.
* @return {string} Returns the csi timing as a string.
*/
pagespeed.PanelLoader.prototype.getCsiTimingsString = function() {
var csiTimingStr = '';
for (var state in this.csiTimings.time) {
csiTimingStr += state + '.' + this.csiTimings.time[state] + ',';
}
for (var state in this.csiTimings.size) {
csiTimingStr += state + '_sz.' + this.csiTimings.size[state] + ',';
}
return csiTimingStr;
};
pagespeed.PanelLoader.prototype['getCsiTimingsString'] =
pagespeed.PanelLoader.prototype.getCsiTimingsString;
/**
* Updates the dashboard.
*/
pagespeed.PanelLoader.prototype.updateDashboard = function() {
var timeNow = new Date();
var dateElem = document.getElementById('dashboard_area') ||
window['dashboard_area'];
if (this.debugIp ||
(window.localStorage && window.localStorage['psa_debug'] == '1')) {
if (!dateElem) {
dateElem = document.createElement('div');
dateElem.id = 'dashboard_area';
dateElem.style.color = 'gray';
dateElem.style.fontSize = '10px';
dateElem.style.fontFace = 'Arial';
dateElem.style.backgroundColor = 'white';
document.body.insertBefore(dateElem, document.body.childNodes[0]);
}
var timings = 'TIME:\n' + JSON.stringify(this.csiTimings.time)
.replace(/["{}]/g, '').replace(/,/g, ' ');
timings += '\nSIZE:\n' + JSON.stringify(this.csiTimings.size)
.replace(/["{}]/g, '').replace(/,/g, ' ');
dateElem.innerHTML =
'<span title="' + timings + '">' + this.dashboardDisplayTime + 'ms; ' +
this.contentSizeKb.toFixed() + 'KB;' + timeNow.toGMTString() +
'</span>';
}
};
/**
* Updates the state and the dashboard with the debugging info.
* @param {string} newState
* @param {number=} opt_size
*/
pagespeed.PanelLoader.prototype.changePageLoadState = function(newState,
opt_size) {
this.state = newState;
var timeNow = new Date();
var timeTaken = (timeNow - this.timeStart);
this.addCsiTiming(newState, timeTaken, opt_size);
if (this.isStateInCriticalPath(newState)) {
this.contentSizeKb += opt_size ? (opt_size / 1024) : 0;
this.dashboardDisplayTime = timeTaken;
}
this.updateDashboard();
};
/**
* Executes above the fold scripts till a panel stub is encountered.
*/
pagespeed.PanelLoader.prototype.executeATFScripts = function() {
if (window.pagespeed && window.pagespeed.deferJs) {
var me = this;
var criticalScriptsDoneCallback = function() {
me.criticalScriptsDone();
};
window.pagespeed.deferJs.registerScriptTags(
criticalScriptsDoneCallback, pagespeed.lastScriptIndexBeforePanelStub);
// TODO(ksimbili): Wait until the high res Images are set before starting
// the execution.
window.pagespeed.deferJs.run();
}
};
// -------------------------- API EXPOSED --------------------------------
/**
* Sets the request from internal ip.
*/
pagespeed.PanelLoader.prototype.setRequestFromInternalIp = function() {
this.debugIp = true;
};
pagespeed.PanelLoader.prototype['setRequestFromInternalIp'] =
pagespeed.PanelLoader.prototype.setRequestFromInternalIp;
/**
* Adds debugging info like timing and size to the dashboard.
* TODO(ksimbili,anupama): Start adding the timing information into pagespeed
* global variables instead of local ones.
* @param {string} variable
* @param {number} time
* @param {number=} opt_size
*/
pagespeed.PanelLoader.prototype.addCsiTiming = function(variable, time,
opt_size) {
this.csiTimings.time[variable] = time;
if (opt_size) this.csiTimings.size[variable] = opt_size;
};
/**
* Loads cookies.
* @param {Array.<string>} data
*/
pagespeed.PanelLoader.prototype.loadCookies = function(data) {
for (var i = 0; i < data.length; i++) {
document.cookie = data[i];
}
};
pagespeed.PanelLoader.prototype['loadCookies'] =
pagespeed.PanelLoader.prototype.loadCookies;
/**
* Attempts to load non-cacheable object. If failed will buffer the object and
* try again after Non-critical is loaded.
* @param {Object} data Non-Critical Data.
*/
pagespeed.PanelLoader.prototype.loadNonCacheableObject = function(data) {
if (this.state == NON_CRITICAL_LOADED) {
return;
}
for (var panelId in data) {
if (!data.hasOwnProperty(panelId)) continue;
this.nonCacheablePanelInstances[panelId] =
this.nonCacheablePanelInstances[panelId] || [];
this.nonCacheablePanelInstances[panelId].push(data[panelId]);
var endPanelStubs = getPanelStubs(getDocument().documentElement,
getDocument().documentElement,
panelId);
if (endPanelStubs.length > 0) {
this.pageManager.instantiatePage(this.nonCacheablePanelInstances);
this.nonCacheablePanelInstances[panelId].pop();
var newInstance = {};
this.nonCacheablePanelInstances[panelId].push(newInstance);
} else {
data[panelId][DONT_BIND] = true;
}
}
};
pagespeed.PanelLoader.prototype['loadNonCacheableObject'] =
pagespeed.PanelLoader.prototype.loadNonCacheableObject;
/**
* Callback function for DeferJs, when critical scripts are done.
*/
pagespeed.PanelLoader.prototype.criticalScriptsDone = function() {
this.readyToLoadNonCritical = true;
this.loadData();
};
pagespeed.PanelLoader.prototype['criticalScriptsDone'] =
pagespeed.PanelLoader.prototype.criticalScriptsDone;
/**
* Buffers the non critical data and loads it if hi res images are loaded.
* @param {Object} data Non-Critical Data.
* @param {boolean=} opt_delay_bind if set to true, will save the data passed in
* and use the saved data that when the function is called again without
* opt_delay_bind (note: will overwrite whatever data is passed in at that
* point with the saved value if something was saved).
*/
pagespeed.PanelLoader.prototype.bufferNonCriticalData = function(
data, opt_delay_bind) {
if (opt_delay_bind) {
this.delayedNonCriticalData = data;
return;
} else if (this.delayedNonCriticalData) {
data = this.delayedNonCriticalData;
}
if (this.state == NON_CRITICAL_LOADED) {
return;
}
this.nonCriticalData = data;
this.nonCriticalDataPresent = true;
this.loadData();
};
pagespeed.PanelLoader.prototype['bufferNonCriticalData'] =
pagespeed.PanelLoader.prototype.bufferNonCriticalData;
/**
* Iniitialize the panel loader.
*/
pagespeed.panelLoaderInit = function() {
if (pagespeed['panelLoader']) {
return;
}
var ctx = new pagespeed.PanelLoader();
pagespeed['panelLoader'] = ctx;
ctx.executeATFScripts();
};
pagespeed['panelLoaderInit'] = pagespeed.panelLoaderInit;