blob: 0079f138cf4835ae77187479a455ce0a666e3d4a [file] [log] [blame]
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Code for detecting and sending to the server the critical CSS
* selectors (selectors applying to any DOM elements on the page) on the client
* side. To use, this script should be injected right before </body> with a call
* to pagespeed.criticalCssBeaconInit(...) appended to it.
*
* @author jud@google.com (Jud Porter)
*/
goog.require('pagespeedutils');
// 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'];
/**
* @constructor
* @param {string} beaconUrl The URL on the server to send the beacon to.
* @param {string} htmlUrl Url of the page the beacon is being inserted on.
* @param {string} optionsHash The hash of the rewrite options. This is required
* to perform the property cache lookup when the beacon is handled by the
* sever.
* @param {string} nonce The nonce sent by the server.
* @param {Array.<string>} selectors List of the selectors on the page.
*/
pagespeed.CriticalCssBeacon = function(beaconUrl, htmlUrl, optionsHash,
nonce, selectors) {
/**
* We divide up the main loop of checkCssSelectors into multiple calls with
* window.setTimeout to minimize the delay in processing other events on the
* page. This constant sets the number of candidate selectors we check in a
* single call to checkCssSelectors.
*
* This value is choosen so that each iteration takes around ~100ms on a
* modern mobile phone. On a nexus 4, each document.querySelector call was
* measured to take around 400us on a complex page.
*
* @const
* @private
*/
this.MAXITERS_ = 250;
this.beaconUrl_ = beaconUrl;
this.htmlUrl_ = htmlUrl;
this.optionsHash_ = optionsHash;
this.nonce_ = nonce;
this.selectors_ = selectors;
this.criticalSelectors_ = [];
this.idx_ = 0;
};
/**
* Send the selectors that have been collected into the criticalSelectors_
* member var back to the server.
* @private
*/
pagespeed.CriticalCssBeacon.prototype.sendBeacon_ = function() {
var data = 'oh=' + this.optionsHash_ + '&n=' + this.nonce_;
data += '&cs=';
for (var i = 0; i < this.criticalSelectors_.length; ++i) {
var tmp = (i > 0) ? ',' : '';
tmp += encodeURIComponent(this.criticalSelectors_[i]);
// TODO(jud): Don't truncate the critical selectors list if we exceed
// MAX_DATA_LEN. Either send a signal back that we exceeded the limit, or
// send multiple beacons back with all the data.
if ((data.length + tmp.length) > pagespeedutils.MAX_POST_SIZE) {
break;
}
data += tmp;
}
// Export the URL for testing purposes.
pagespeed['criticalCssBeaconData'] = data;
// TODO(jud): This beacon should coordinate with the add_instrumentation JS
// so that only one beacon request is sent if both filters are enabled.
pagespeedutils.sendBeacon(this.beaconUrl_, this.htmlUrl_, data);
};
/**
* Check if CSS selectors apply to DOM elements that are visible on initial page
* load.
* @param {Function} callback Function to call when calculating the selectors
* has finished.
* @private
*/
pagespeed.CriticalCssBeacon.prototype.checkCssSelectors_ = function(callback) {
for (var i = 0; i < this.MAXITERS_ && this.idx_ < this.selectors_.length;
++i, ++this.idx_) {
try {
// If this selector matched any DOM elements, then consider it critical.
if (document.querySelector(this.selectors_[this.idx_]) != null) {
this.criticalSelectors_.push(this.selectors_[this.idx_]);
}
} catch (e) {
// SYNTAX_ERR is thrown if the browser can't parse a selector (eg, CSS3 in
// a CSS2.1 browser). Ignore these exceptions.
// TODO(jud): Consider if continue is the right thing to do here. It may
// be safer to mark this selector as critical if the browser didn't
// understand it.
continue;
}
}
if (this.idx_ < this.selectors_.length) {
window.setTimeout(this.checkCssSelectors_.bind(this), 0, callback);
} else {
callback();
}
};
/**
* Initialize.
* @param {string} beaconUrl The URL on the server to send the beacon to.
* @param {string} htmlUrl Url of the page the beacon is being inserted on.
* @param {string} optionsHash The hash of the rewrite options.
* @param {string} nonce The nonce sent by the server.
* @param {Array.<string>} selectors List of the selectors on the page.
*/
pagespeed.criticalCssBeaconInit = function(beaconUrl, htmlUrl, optionsHash,
nonce, selectors) {
// Verify that the browser supports the APIs we need and bail out early if we
// don't.
if (!document.querySelector || !Function.prototype.bind) {
return;
}
var temp = new pagespeed.CriticalCssBeacon(beaconUrl, htmlUrl, optionsHash,
nonce, selectors);
// Add event to the onload handler to scan selectors and beacon back which
// apply to critical elements.
var beacon_onload = function() {
// Attempt not to block other onload events on the page by wrapping in
// setTimeout().
window.setTimeout(function() {
temp.checkCssSelectors_(function() {
temp.sendBeacon_();
});
}, 0);
};
pagespeedutils.addHandler(window, 'load', beacon_onload);
};
pagespeed['criticalCssBeaconInit'] = pagespeed.criticalCssBeaconInit;