fileMinifier.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. var debug = require('debug')('ylt:fileMinifier');
  2. var Q = require('q');
  3. var UglifyJS = require('uglify-js');
  4. var CleanCSS = require('clean-css');
  5. var Minimize = require('minimize');
  6. var FileMinifier = function() {
  7. function minifyFile(entry) {
  8. var deferred = Q.defer();
  9. if (!entry.weightCheck || !entry.weightCheck.body) {
  10. // No valid file available
  11. deferred.resolve(entry);
  12. return deferred.promise;
  13. }
  14. var fileSize = entry.weightCheck.uncompressedSize;
  15. debug('Let\'s try to optimize %s', entry.url);
  16. debug('Current file size is %d', fileSize);
  17. var startTime = Date.now();
  18. if (entry.isJS && !isKnownAsMinified(entry.url) && !looksAlreadyMinified(entry.weightCheck.body)) {
  19. debug('File is a JS');
  20. return minifyJs(entry.weightCheck.body)
  21. .then(function(newFile) {
  22. if (!newFile) {
  23. debug('Optimization didn\'t work');
  24. return entry;
  25. }
  26. var endTime = Date.now();
  27. var newFileSize = newFile.length;
  28. debug('JS minification complete for %s', entry.url);
  29. if (gainIsEnough(fileSize, newFileSize)) {
  30. entry.weightCheck.bodyAfterOptimization = newFile;
  31. entry.weightCheck.optimized = newFileSize;
  32. entry.weightCheck.isOptimized = false;
  33. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  34. }
  35. return entry;
  36. })
  37. .fail(function(err) {
  38. return entry;
  39. });
  40. } else if (entry.isCSS) {
  41. debug('File is a CSS');
  42. return minifyCss(entry.weightCheck.body)
  43. .then(function(newFile) {
  44. if (!newFile) {
  45. debug('Optimization didn\'t work');
  46. return entry;
  47. }
  48. var endTime = Date.now();
  49. debug('CSS minification took %dms', endTime - startTime);
  50. var newFileSize = newFile.length;
  51. debug('CSS minification complete for %s', entry.url);
  52. if (gainIsEnough(fileSize, newFileSize)) {
  53. entry.weightCheck.bodyAfterOptimization = newFile;
  54. entry.weightCheck.optimized = newFileSize;
  55. entry.weightCheck.isOptimized = false;
  56. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  57. }
  58. return entry;
  59. })
  60. .fail(function(err) {
  61. return entry;
  62. });
  63. } else if (entry.isHTML) {
  64. debug('File is an HTML');
  65. return minifyHtml(entry.weightCheck.body)
  66. .then(function(newFile) {
  67. if (!newFile) {
  68. debug('Optimization didn\'t work');
  69. return entry;
  70. }
  71. var endTime = Date.now();
  72. debug('HTML minification took %dms', endTime - startTime);
  73. var newFileSize = newFile.length;
  74. debug('HTML minification complete for %s', entry.url);
  75. if (gainIsEnough(fileSize, newFileSize)) {
  76. entry.weightCheck.bodyAfterOptimization = newFile;
  77. entry.weightCheck.optimized = newFileSize;
  78. entry.weightCheck.isOptimized = false;
  79. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  80. }
  81. return entry;
  82. })
  83. .fail(function(err) {
  84. return entry;
  85. });
  86. } else {
  87. debug('Not minfiable type or already minified');
  88. deferred.resolve(entry);
  89. }
  90. return deferred.promise;
  91. }
  92. // The gain is estimated of enough value if it's over 2KB or over 20%,
  93. // but it's ignored if is below 400 bytes
  94. function gainIsEnough(oldWeight, newWeight) {
  95. var gain = oldWeight - newWeight;
  96. var ratio = gain / oldWeight;
  97. return (gain > 2096 || (ratio > 0.2 && gain > 400));
  98. }
  99. // Uglify
  100. function minifyJs(body) {
  101. // Splitting the Uglify function because it sometime takes too long (more than 10 seconds)
  102. // I hope that, by splitting, it can be a little more asynchronous, so the application doesn't freeze.
  103. return splittedUglifyStep1(body)
  104. .delay(1)
  105. .then(splittedUglifyStep2)
  106. .delay(1)
  107. .then(splittedUglifyStep3)
  108. .delay(1)
  109. .then(splittedUglifyStep4)
  110. .delay(1)
  111. .then(splittedUglifyStep5)
  112. .delay(1)
  113. .then(splittedUglifyStep6)
  114. .delay(1)
  115. .then(splittedUglifyStep7);
  116. }
  117. function splittedUglifyStep1(code) {
  118. var deferred = Q.defer();
  119. var startTime = Date.now();
  120. try {
  121. var toplevel_ast = UglifyJS.parse(code);
  122. var endTime = Date.now();
  123. debug('Uglify step 1 took %dms', endTime - startTime);
  124. deferred.resolve(toplevel_ast);
  125. } catch(err) {
  126. debug('JS syntax error, Uglify\'s parser failed (step 1)');
  127. deferred.reject(err);
  128. }
  129. return deferred.promise;
  130. }
  131. function splittedUglifyStep2(toplevel) {
  132. var deferred = Q.defer();
  133. var startTime = Date.now();
  134. toplevel.figure_out_scope();
  135. var endTime = Date.now();
  136. debug('Uglify step 2 took %dms', endTime - startTime);
  137. deferred.resolve(toplevel);
  138. return deferred.promise;
  139. }
  140. function splittedUglifyStep3(toplevel) {
  141. var deferred = Q.defer();
  142. var startTime = Date.now();
  143. var compressor = UglifyJS.Compressor({warnings: false});
  144. var compressed_ast = toplevel.transform(compressor);
  145. var endTime = Date.now();
  146. debug('Uglify step 3 took %dms', endTime - startTime);
  147. deferred.resolve(compressed_ast);
  148. return deferred.promise;
  149. }
  150. function splittedUglifyStep4(compressed_ast) {
  151. var deferred = Q.defer();
  152. var startTime = Date.now();
  153. compressed_ast.figure_out_scope();
  154. var endTime = Date.now();
  155. debug('Uglify step 4 took %dms', endTime - startTime);
  156. deferred.resolve(compressed_ast);
  157. return deferred.promise;
  158. }
  159. function splittedUglifyStep5(compressed_ast) {
  160. var deferred = Q.defer();
  161. var startTime = Date.now();
  162. compressed_ast.compute_char_frequency();
  163. var endTime = Date.now();
  164. debug('Uglify step 5 took %dms', endTime - startTime);
  165. deferred.resolve(compressed_ast);
  166. return deferred.promise;
  167. }
  168. function splittedUglifyStep6(compressed_ast) {
  169. var deferred = Q.defer();
  170. var startTime = Date.now();
  171. compressed_ast.mangle_names();
  172. var endTime = Date.now();
  173. debug('Uglify step 6 took %dms', endTime - startTime);
  174. deferred.resolve(compressed_ast);
  175. return deferred.promise;
  176. }
  177. function splittedUglifyStep7(compressed_ast) {
  178. var deferred = Q.defer();
  179. var startTime = Date.now();
  180. var code = compressed_ast.print_to_string();
  181. var endTime = Date.now();
  182. debug('Uglify step 7 took %dms', endTime - startTime);
  183. deferred.resolve(code);
  184. return deferred.promise;
  185. }
  186. // Clear-css
  187. function minifyCss(body) {
  188. var deferred = Q.defer();
  189. try {
  190. var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
  191. deferred.resolve(result.styles);
  192. } catch(err) {
  193. deferred.reject(err);
  194. }
  195. return deferred.promise;
  196. }
  197. // HTMLMinifier
  198. function minifyHtml(body) {
  199. var deferred = Q.defer();
  200. var minimize = new Minimize({
  201. empty: true, // KEEP empty attributes
  202. conditionals: true, // KEEP conditional internet explorer comments
  203. spare: true // KEEP redundant attributes
  204. });
  205. minimize.parse(body, function (error, data) {
  206. if (error) {
  207. deferred.reject(error);
  208. } else {
  209. deferred.resolve(data);
  210. }
  211. });
  212. return deferred.promise;
  213. }
  214. // Avoid loosing time trying to compress some JS libraries known as already compressed
  215. function isKnownAsMinified(url) {
  216. var result = false;
  217. // Twitter
  218. result = result || /^https?:\/\/platform\.twitter\.com\/widgets\.js/.test(url);
  219. // Facebook
  220. result = result || /^https:\/\/connect\.facebook\.net\/[^\/]*\/(sdk|all)\.js/.test(url);
  221. // Google +1
  222. result = result || /^https:\/\/apis\.google\.com\/js\/plusone\.js/.test(url);
  223. // jQuery CDN
  224. result = result || /^https?:\/\/code\.jquery\.com\/.*\.min.js/.test(url);
  225. // Google Analytics
  226. result = result || /^https?:\/\/(www|ssl)\.google-analytics\.com\/(.*)\.js/.test(url);
  227. if (result === true) {
  228. debug('This file is known as already minified. Skipping minification: %s', url);
  229. }
  230. return result;
  231. }
  232. // Avoid loosing tome trying to compress JS files if they alreay look minified
  233. // by counting the number of lines compared to the total size.
  234. // Less than 1000kb per line is suspicious
  235. function looksAlreadyMinified(code) {
  236. var linesCount = code.split(/\r\n|\r|\n/).length;
  237. var linesRatio = code.length / linesCount;
  238. var looksMinified = linesRatio > 1024;
  239. debug('Lines ratio is %d bytes per line', Math.round(linesRatio));
  240. debug(looksMinified ? 'It looks already minified' : 'It doesn\'t look minified');
  241. return looksMinified;
  242. }
  243. function entryTypeCanBeMinified(entry) {
  244. return entry.isJS || entry.isCSS || entry.isHTML;
  245. }
  246. return {
  247. minifyFile: minifyFile,
  248. minifyJs: minifyJs,
  249. minifyCss: minifyCss,
  250. minifyHtml: minifyHtml,
  251. gainIsEnough: gainIsEnough,
  252. entryTypeCanBeMinified: entryTypeCanBeMinified,
  253. isKnownAsMinified: isKnownAsMinified
  254. };
  255. };
  256. module.exports = new FileMinifier();