123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- /*
- * Redownloading every files after Phantomas has finished
- * Checks weight and every kind of compression
- *
- */
- var debug = require('debug')('ylt:weightChecker');
- var Q = require('q');
- var http = require('http');
- var zlib = require('zlib');
- var async = require('async');
- var request = require('request');
- var imageOptimizer = require('./imageOptimizer');
- var fileMinifier = require('./fileMinifier');
- var gzipCompressor = require('./gzipCompressor');
- var WeightChecker = function() {
- var MAX_PARALLEL_DOWNLOADS = 10;
- var REQUEST_TIMEOUT = 15000; // 15 seconds
- // This function will re-download every asset and check if it could be optimized
- function recheckAllFiles(data) {
- var startTime = Date.now();
- debug('Redownload started');
- var deferred = Q.defer();
- var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList);
- delete data.toolsResults.phantomas.metrics.requestsList;
- delete data.toolsResults.phantomas.offenders.requestsList;
- // Transform every request into a download function with a callback when done
- var redownloadList = requestsList.map(function(entry) {
- return function(callback) {
-
- redownloadEntry(entry)
- .then(imageOptimizer.optimizeImage)
- .then(fileMinifier.minifyFile)
- .then(gzipCompressor.compressFile)
- .then(function(newEntry) {
- callback(null, newEntry);
- })
- .fail(function(err) {
- callback(err);
- });
- };
- });
- // Lanch all redownload functions and wait for completion
- async.parallelLimit(redownloadList, MAX_PARALLEL_DOWNLOADS, function(err, results) {
-
- if (err) {
- debug(err);
- deferred.reject(err);
- } else {
- debug('All files checked');
- endTime = Date.now();
- debug('Redownload took %d ms', endTime - startTime);
-
- var metrics = {};
- var offenders = {};
- // Count requests
- offenders.totalRequests = listRequestsByType(results);
- metrics.totalRequests = offenders.totalRequests.total;
- // Remove unwanted requests (redirections, about:blank)
- results = results.filter(function(result) {
- return (result !== null && result.weightCheck && result.weightCheck.bodySize > 0);
- });
- // Total weight
- offenders.totalWeight = listRequestWeight(results);
- metrics.totalWeight = offenders.totalWeight.totalWeight;
- // Image compression
- offenders.imageOptimization = listImageNotOptimized(results);
- metrics.imageOptimization = offenders.imageOptimization.totalGain;
- // File minification
- offenders.fileMinification = listFilesNotMinified(results);
- metrics.fileMinification = offenders.fileMinification.totalGain;
- // Gzip compression
- offenders.gzipCompression = listFilesNotGzipped(results);
- metrics.gzipCompression = offenders.gzipCompression.totalGain;
- // Small requests
- offenders.smallRequests = listSmallRequests(results);
- metrics.smallRequests = offenders.smallRequests.total;
- data.toolsResults.weightChecker = {
- metrics: metrics,
- offenders: offenders
- };
- deferred.resolve(data);
- }
- });
- return deferred.promise;
- }
- function listRequestWeight(requests) {
- var results = {
- totalWeight: 0,
- byType: {
- html: {
- totalWeight: 0,
- requests: []
- },
- css: {
- totalWeight: 0,
- requests: []
- },
- js: {
- totalWeight: 0,
- requests: []
- },
- json: {
- totalWeight: 0,
- requests: []
- },
- image: {
- totalWeight: 0,
- requests: []
- },
- video: {
- totalWeight: 0,
- requests: []
- },
- webfont: {
- totalWeight: 0,
- requests: []
- },
- other: {
- totalWeight: 0,
- requests: []
- }
- }
- };
- requests.forEach(function(req) {
- var weight = ((typeof req.weightCheck.bodySize === 'number') ? req.weightCheck.bodySize + req.weightCheck.headersSize : req.contentLength) || 0;
- var type = req.type || 'other';
- results.totalWeight += weight;
- results.byType[type].totalWeight += weight;
- results.byType[type].requests.push({
- url: req.url,
- weight: weight
- });
- });
- return results;
- }
- function listImageNotOptimized(requests) {
- var results = {
- totalGain: 0,
- images: []
- };
- requests.forEach(function(req) {
- if (req.weightCheck.bodySize > 0 && imageOptimizer.entryTypeCanBeOptimized(req) && req.weightCheck.isOptimized === false) {
- var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
- var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
- var gain = before - after;
- if (gain > 200) {
- results.totalGain += gain;
- results.images.push({
- url: req.url,
- original: req.weightCheck.bodySize,
- isCompressed: req.weightCheck.isCompressed,
- afterCompression: req.weightCheck.afterCompression,
- afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
- lossless: req.weightCheck.lossless,
- lossy: req.weightCheck.lossy,
- gain: gain
- });
- }
- }
- });
- return results;
- }
- function listFilesNotMinified(requests) {
- var results = {
- totalGain: 0,
- files: []
- };
- requests.forEach(function(req) {
- if (req.weightCheck.bodySize > 0 && fileMinifier.entryTypeCanBeMinified(req) && req.weightCheck.isOptimized === false) {
- var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
- var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
- var gain = before - after;
- if (gain > 200) {
- results.totalGain += gain;
- results.files.push({
- url: req.url,
- original: req.weightCheck.bodySize,
- isCompressed: req.weightCheck.isCompressed,
- afterCompression: req.weightCheck.afterCompression,
- afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
- optimized: req.weightCheck.optimized,
- gain: gain
- });
- }
- }
- });
- return results;
- }
- function listFilesNotGzipped(requests) {
- var results = {
- totalGain: 0,
- files: []
- };
- requests.forEach(function(req) {
- if (req.weightCheck.uncompressedSize && req.weightCheck.isCompressed === false && req.weightCheck.afterCompression) {
- var gain = req.weightCheck.uncompressedSize - req.weightCheck.afterCompression;
- results.totalGain += gain;
- results.files.push({
- url: req.url,
- original: req.weightCheck.uncompressedSize,
- gzipped: req.weightCheck.afterCompression,
- gain: gain
- });
- }
- });
- return results;
- }
- function listRequestsByType(requests) {
- var results = {
- total: 0,
- byType: {
- html: [],
- css: [],
- js: [],
- json: [],
- image: [],
- video: [],
- webfont: [],
- other: []
- }
- };
- requests.forEach(function(req) {
- if (req.url !== 'about:blank') {
- var type = req.type || 'other';
- results.byType[type].push(req.url);
- results.total ++;
- }
- });
- return results;
- }
- function listSmallRequests(requests) {
- var results = {
- total: 0,
- byType: {
- css: [],
- js: [],
- image: []
- }
- };
- requests.forEach(function(req) {
- if (req.weightCheck.bodySize > 0 && req.weightCheck.bodySize < 2048) {
- if (req.isCSS || req.isJS || req.isImage) {
- results.byType[req.type].push({
- url: req.url,
- size: req.weightCheck.bodySize
- });
- results.total ++;
- }
- }
- });
- return results;
- }
- function redownloadEntry(entry) {
- var deferred = Q.defer();
-
- function downloadError(message) {
- debug('Could not download %s Error: %s', entry.url, message);
- entry.weightCheck = {
- message: message
- };
- deferred.resolve(entry);
- }
- // Not downloaded again but will be counted in totalWeight
- function notDownloadableFile(message) {
- entry.weightCheck = {
- message: message
- };
- deferred.resolve(entry);
- }
- // Not counted in totalWeight
- function unwantedFile(message) {
- debug(message);
- deferred.resolve(entry);
- }
- if (entry.method !== 'GET') {
- notDownloadableFile('only downloading GET');
- return deferred.promise;
- }
- if (entry.status !== 200) {
- unwantedFile('only downloading requests with status code 200');
- return deferred.promise;
- }
- if (entry.url === 'about:blank') {
- unwantedFile('not downloading about:blank');
- return deferred.promise;
- }
- debug('Downloading %s', entry.url);
- // Always add a gzip header before sending, in case the server listens to it
- var reqHeaders = entry.requestHeaders;
- reqHeaders['Accept-Encoding'] = 'gzip, deflate';
- var requestOptions = {
- method: entry.method,
- url: entry.url,
- headers: reqHeaders,
- timeout: REQUEST_TIMEOUT
- };
- download(requestOptions, entry.contentType, function(error, result) {
- if (error) {
- if (error.code === 'ETIMEDOUT') {
- downloadError('timeout after ' + REQUEST_TIMEOUT + 'ms');
- } else {
- downloadError('error while downloading: ' + error.code);
- }
- return;
- }
-
- debug('%s downloaded correctly', entry.url);
- entry.weightCheck = result;
- deferred.resolve(entry);
- });
- return deferred.promise;
- }
- // Inspired by https://github.com/cvan/fastHAR-api/blob/10cec585/app.js
- function download(requestOptions, contentType, callback) {
- var statusCode;
- request(requestOptions)
- .on('response', function(res) {
-
- // Raw headers were added in NodeJS v0.12
- // (https://github.com/joyent/node/issues/4844), but let's
- // reconstruct them for backwards compatibility.
- var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
- ' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
- Object.keys(res.headers).forEach(function(headerKey) {
- rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
- });
- rawHeaders += '\r\n';
- var uncompressedSize = 0; // size after uncompression
- var bodySize = 0; // bytes size over the wire
- var body = ''; // plain text body (after uncompressing gzip/deflate)
- var isCompressed = false;
- function tally() {
- if (statusCode !== 200) {
- callback({code: statusCode});
- return;
- }
- var result = {
- body: body,
- headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
- bodySize: bodySize,
- isCompressed: isCompressed,
- uncompressedSize: uncompressedSize
- };
- callback(null, result);
- }
- switch (res.headers['content-encoding']) {
- case 'gzip':
- var gzip = zlib.createGunzip();
- gzip.on('data', function (data) {
- body += data;
- uncompressedSize += data.length;
- }).on('end', function () {
- isCompressed = true;
- tally();
- }).on('error', function(err) {
- debug(err);
- });
- res.on('data', function (data) {
- bodySize += data.length;
- }).pipe(gzip);
- break;
- case 'deflate':
- res.setEncoding('utf8');
- var deflate = zlib.createInflate();
- deflate.on('data', function (data) {
- body += data;
- uncompressedSize += data.length;
- }).on('end', function () {
- isCompressed = true;
- tally();
- }).on('error', function(err) {
- debug(err);
- });
- res.on('data', function (data) {
- bodySize += data.length;
- }).pipe(deflate);
- break;
- default:
- if (contentType === 'image/jpeg' || contentType === 'image/png') {
- res.setEncoding('binary');
- }
- res.on('data', function (data) {
- body += data;
- uncompressedSize += data.length;
- bodySize += data.length;
- }).on('end', function () {
- tally();
- });
- break;
- }
- })
- .on('response', function(response) {
- statusCode = response.statusCode;
- })
- .on('error', function(err) {
- callback(err);
- });
- }
- return {
- recheckAllFiles: recheckAllFiles,
- listRequestWeight: listRequestWeight,
- redownloadEntry: redownloadEntry,
- download: download
- };
- };
- module.exports = new WeightChecker();
|