weightChecker.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. /*
  2. * Redownloading every files after Phantomas has finished
  3. * Checks weight and every kind of compression
  4. *
  5. */
  6. var debug = require('debug')('ylt:weightChecker');
  7. var Q = require('q');
  8. var http = require('http');
  9. var zlib = require('zlib');
  10. var async = require('async');
  11. var request = require('request');
  12. var imageOptimizer = require('./imageOptimizer');
  13. var WeightChecker = function() {
  14. var MAX_PARALLEL_DOWNLOADS = 10;
  15. var REQUEST_TIMEOUT = 10000; // 10 seconds
  16. // This function will re-download every asset and check if it could be optimized
  17. function recheckAllFiles(data) {
  18. var deferred = Q.defer();
  19. var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList);
  20. delete data.toolsResults.phantomas.metrics.requestsList;
  21. delete data.toolsResults.phantomas.offenders.requestsList;
  22. // Transform every request into a download function with a callback when done
  23. var redownloadList = requestsList.map(function(entry) {
  24. return function(callback) {
  25. redownloadEntry(entry)
  26. .then(imageOptimizer.recompressIfImage)
  27. .then(function(newEntry) {
  28. callback(null, newEntry);
  29. })
  30. .fail(function(err) {
  31. callback(err);
  32. });
  33. };
  34. });
  35. // Lanch all redownload functions and wait for completion
  36. async.parallelLimit(redownloadList, MAX_PARALLEL_DOWNLOADS, function(err, results) {
  37. if (err) {
  38. debug(err);
  39. deferred.reject(err);
  40. } else {
  41. debug('All files checked');
  42. var metrics = {};
  43. var offenders = {};
  44. // Total weight
  45. offenders.totalWeight = listRequestWeight(results);
  46. metrics.totalWeight = offenders.totalWeight.totalWeight;
  47. // Image compression
  48. offenders.imageOptimization = listImageNotOptimized(results);
  49. metrics.imageOptimization = offenders.imageOptimization.totalGain;
  50. data.toolsResults.weightChecker = {
  51. metrics: metrics,
  52. offenders: offenders
  53. };
  54. deferred.resolve(data);
  55. }
  56. });
  57. return deferred.promise;
  58. }
  59. function listRequestWeight(requests) {
  60. var results = {
  61. totalWeight: 0,
  62. byType: {
  63. html: {
  64. totalWeight: 0,
  65. requests: []
  66. },
  67. css: {
  68. totalWeight: 0,
  69. requests: []
  70. },
  71. js: {
  72. totalWeight: 0,
  73. requests: []
  74. },
  75. json: {
  76. totalWeight: 0,
  77. requests: []
  78. },
  79. image: {
  80. totalWeight: 0,
  81. requests: []
  82. },
  83. video: {
  84. totalWeight: 0,
  85. requests: []
  86. },
  87. webfont: {
  88. totalWeight: 0,
  89. requests: []
  90. },
  91. other: {
  92. totalWeight: 0,
  93. requests: []
  94. }
  95. }
  96. };
  97. requests.forEach(function(req) {
  98. var weight = ((typeof req.weightCheck.bodySize === 'number') ? req.weightCheck.bodySize + req.weightCheck.headersSize : req.contentLength) || 0;
  99. var type = req.type || 'other';
  100. results.totalWeight += weight;
  101. results.byType[type].totalWeight += weight;
  102. results.byType[type].requests.push({
  103. url: req.url,
  104. weight: weight
  105. });
  106. });
  107. return results;
  108. }
  109. function listImageNotOptimized(requests) {
  110. var results = {
  111. totalGain: 0,
  112. images: []
  113. };
  114. requests.forEach(function(req) {
  115. if (req.weightCheck.bodySize && req.weightCheck.isOptimized === false) {
  116. var gain = req.weightCheck.bodySize - req.weightCheck.optimized;
  117. results.totalGain += gain;
  118. results.images.push({
  119. url: req.url,
  120. original: req.weightCheck.bodySize,
  121. optimized: req.weightCheck.optimized,
  122. lossless: req.weightCheck.lossless,
  123. lossy: req.weightCheck.lossy,
  124. gain: gain
  125. });
  126. }
  127. });
  128. return results;
  129. }
  130. function redownloadEntry(entry) {
  131. var deferred = Q.defer();
  132. function onError(message) {
  133. debug('Could not download %s Error: %s', entry.url, message);
  134. entry.weightCheck = {
  135. message: message
  136. };
  137. deferred.resolve(entry);
  138. }
  139. if (entry.method !== 'GET') {
  140. onError('only downloading GET');
  141. return deferred.promise;
  142. }
  143. if (entry.status !== 200) {
  144. onError('only downloading requests with status code 200');
  145. return deferred.promise;
  146. }
  147. debug('Downloading %s', entry.url);
  148. // Always add a gzip header before sending, in case the server listens to it
  149. var reqHeaders = entry.requestHeaders;
  150. reqHeaders['Accept-Encoding'] = 'gzip, deflate';
  151. var requestOptions = {
  152. method: entry.method,
  153. url: entry.url,
  154. headers: reqHeaders,
  155. timeout: REQUEST_TIMEOUT
  156. //encoding: (entry.contentType === 'image/jpeg' || entry.contentType === 'image/png') ? 'binary' : null
  157. };
  158. download(requestOptions, entry.contentType, function(error, result) {
  159. if (error) {
  160. if (error.code === 'ETIMEDOUT') {
  161. onError('timeout after ' + REQUEST_TIMEOUT + 'ms');
  162. } else {
  163. onError('error while downloading: ' + error.code);
  164. }
  165. return;
  166. }
  167. debug('%s downloaded correctly', entry.url);
  168. entry.weightCheck = result;
  169. deferred.resolve(entry);
  170. });
  171. return deferred.promise;
  172. }
  173. // Inspired by https://github.com/cvan/fastHAR-api/blob/10cec585/app.js
  174. function download(requestOptions, contentType, callback) {
  175. var statusCode;
  176. request(requestOptions)
  177. .on('response', function(res) {
  178. // Raw headers were added in NodeJS v0.12
  179. // (https://github.com/joyent/node/issues/4844), but let's
  180. // reconstruct them for backwards compatibility.
  181. var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
  182. ' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
  183. Object.keys(res.headers).forEach(function(headerKey) {
  184. rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
  185. });
  186. rawHeaders += '\r\n';
  187. var uncompressedSize = 0; // size after uncompression
  188. var bodySize = 0; // bytes size over the wire
  189. var body = ''; // plain text body (after uncompressing gzip/deflate)
  190. var isCompressed = false;
  191. function tally() {
  192. if (statusCode !== 200) {
  193. callback({code: statusCode});
  194. return;
  195. }
  196. var result = {
  197. body: body,
  198. headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
  199. bodySize: bodySize,
  200. isCompressed: isCompressed,
  201. uncompressedSize: uncompressedSize
  202. };
  203. callback(null, result);
  204. }
  205. switch (res.headers['content-encoding']) {
  206. case 'gzip':
  207. var gzip = zlib.createGunzip();
  208. gzip.on('data', function (data) {
  209. body += data;
  210. uncompressedSize += data.length;
  211. }).on('end', function () {
  212. isCompressed = true;
  213. tally();
  214. }).on('error', function(err) {
  215. debug(err);
  216. });
  217. res.on('data', function (data) {
  218. bodySize += data.length;
  219. }).pipe(gzip);
  220. break;
  221. case 'deflate':
  222. res.setEncoding('utf8');
  223. var deflate = zlib.createInflate();
  224. deflate.on('data', function (data) {
  225. body += data;
  226. uncompressedSize += data.length;
  227. }).on('end', function () {
  228. isCompressed = true;
  229. tally();
  230. }).on('error', function(err) {
  231. debug(err);
  232. });
  233. res.on('data', function (data) {
  234. bodySize += data.length;
  235. }).pipe(deflate);
  236. break;
  237. default:
  238. if (contentType === 'image/jpeg' || contentType === 'image/png') {
  239. res.setEncoding('binary');
  240. }
  241. res.on('data', function (data) {
  242. body += data;
  243. uncompressedSize += data.length;
  244. bodySize += data.length;
  245. }).on('end', function () {
  246. tally();
  247. });
  248. break;
  249. }
  250. })
  251. .on('response', function(response) {
  252. statusCode = response.statusCode;
  253. })
  254. .on('error', function(err) {
  255. callback(err);
  256. });
  257. }
  258. return {
  259. recheckAllFiles: recheckAllFiles,
  260. listRequestWeight: listRequestWeight,
  261. redownloadEntry: redownloadEntry,
  262. download: download
  263. };
  264. };
  265. module.exports = new WeightChecker();