fileMinifier.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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 htmlMinifier = require('html-minifier');
  6. var FileMinifier = function() {
  7. function minifyFile(entry) {
  8. var deferred = Q.defer();
  9. if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
  10. // No valid file available
  11. deferred.resolve(entry);
  12. return deferred.promise;
  13. }
  14. var fileSize = entry.weightCheck.uncompressedSize;
  15. var bodyString = entry.weightCheck.bodyBuffer.toString();
  16. debug('Let\'s try to optimize %s', entry.url);
  17. debug('Current file size is %d', fileSize);
  18. var startTime = Date.now();
  19. if (entry.isJS && !isKnownAsMinified(entry.url) && !looksAlreadyMinified(bodyString)) {
  20. debug('File is a JS');
  21. return minifyJs(bodyString)
  22. .then(function(newFile) {
  23. if (!newFile) {
  24. debug('Optimization didn\'t work');
  25. return entry;
  26. }
  27. var endTime = Date.now();
  28. var newFileSize = newFile.length;
  29. debug('JS minification complete for %s', entry.url);
  30. if (gainIsEnough(fileSize, newFileSize)) {
  31. entry.weightCheck.bodyAfterOptimization = newFile;
  32. entry.weightCheck.optimized = newFileSize;
  33. entry.weightCheck.isOptimized = false;
  34. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  35. }
  36. return entry;
  37. })
  38. .fail(function(err) {
  39. return entry;
  40. });
  41. } else if (entry.isCSS) {
  42. debug('File is a CSS');
  43. return minifyCss(entry.weightCheck.bodyBuffer.toString())
  44. .then(function(newFile) {
  45. if (!newFile) {
  46. debug('Optimization didn\'t work');
  47. return entry;
  48. }
  49. var endTime = Date.now();
  50. debug('CSS minification took %dms', endTime - startTime);
  51. var newFileSize = newFile.length;
  52. debug('CSS minification complete for %s', entry.url);
  53. if (gainIsEnough(fileSize, newFileSize)) {
  54. entry.weightCheck.bodyAfterOptimization = newFile;
  55. entry.weightCheck.optimized = newFileSize;
  56. entry.weightCheck.isOptimized = false;
  57. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  58. }
  59. return entry;
  60. })
  61. .fail(function(err) {
  62. return entry;
  63. });
  64. } else if (entry.isHTML) {
  65. debug('File is an HTML');
  66. return minifyHtml(entry.weightCheck.bodyBuffer.toString())
  67. .then(function(newFile) {
  68. if (!newFile) {
  69. debug('Optimization didn\'t work');
  70. return entry;
  71. }
  72. var endTime = Date.now();
  73. debug('HTML minification took %dms', endTime - startTime);
  74. var newFileSize = newFile.length;
  75. debug('HTML minification complete for %s', entry.url);
  76. if (gainIsEnough(fileSize, newFileSize)) {
  77. entry.weightCheck.bodyAfterOptimization = newFile;
  78. entry.weightCheck.optimized = newFileSize;
  79. entry.weightCheck.isOptimized = false;
  80. debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
  81. }
  82. return entry;
  83. })
  84. .fail(function(err) {
  85. return entry;
  86. });
  87. } else {
  88. debug('Not minifiable type or already minified');
  89. deferred.resolve(entry);
  90. }
  91. return deferred.promise;
  92. }
  93. // The gain is estimated of enough value if it's over 2KB or over 20%,
  94. // but it's ignored if is below 400 bytes
  95. function gainIsEnough(oldWeight, newWeight) {
  96. var gain = oldWeight - newWeight;
  97. var ratio = gain / oldWeight;
  98. return (gain > 2096 || (ratio > 0.2 && gain > 400));
  99. }
  100. // Uglify
  101. function minifyJs(body) {
  102. var deferred = Q.defer();
  103. var startTime = Date.now();
  104. var result = UglifyJS.minify(body, {
  105. // Only do the compression step for smaller files
  106. // otherwise it can take a very long time compared to the gain
  107. compress: (body.length < 200*1024)
  108. });
  109. var endTime = Date.now();
  110. debug('Uglify took %dms', endTime - startTime);
  111. deferred.resolve(result.code);
  112. return deferred.promise;
  113. }
  114. // Clear-css
  115. function minifyCss(body) {
  116. var deferred = Q.defer();
  117. try {
  118. var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
  119. deferred.resolve(result.styles);
  120. } catch(err) {
  121. deferred.reject(err);
  122. }
  123. return deferred.promise;
  124. }
  125. // HTMLMinifier
  126. function minifyHtml(body) {
  127. var deferred = Q.defer();
  128. try {
  129. var result = htmlMinifier.minify(body, {
  130. collapseWhitespace: true,
  131. conservativeCollapse: true,
  132. continueOnParseError: true,
  133. decodeEntities: true,
  134. minifyCSS: true,
  135. minifyJS: true,
  136. preserveLineBreaks: true,
  137. removeAttributeQuotes: true,
  138. removeComments: true,
  139. removeScriptTypeAttributes: true,
  140. removeStyleLinkTypeAttributes: true
  141. });
  142. deferred.resolve(result);
  143. } catch(err) {
  144. deferred.reject(err);
  145. }
  146. return deferred.promise;
  147. }
  148. // Avoid losing time trying to compress some JS libraries known as already compressed
  149. function isKnownAsMinified(url) {
  150. var result = false;
  151. // Twitter
  152. result = result || /^https?:\/\/platform\.twitter\.com\/widgets\.js/.test(url);
  153. // Facebook
  154. result = result || /^https:\/\/connect\.facebook\.net\/[^\/]*\/(sdk|all)\.js/.test(url);
  155. // Google +1
  156. result = result || /^https:\/\/apis\.google\.com\/js\/plusone\.js/.test(url);
  157. // jQuery CDN
  158. result = result || /^https?:\/\/code\.jquery\.com\/.*\.min.js/.test(url);
  159. // Google Analytics
  160. result = result || /^https?:\/\/(www|ssl)\.google-analytics\.com\/(.*)\.js/.test(url);
  161. if (result === true) {
  162. debug('This file is known as already minified. Skipping minification: %s', url);
  163. }
  164. return result;
  165. }
  166. // Avoid losing time trying to compress JS files if they already look minified
  167. // by counting the number of lines compared to the total size.
  168. // Less than 2KB per line is suspicious
  169. function looksAlreadyMinified(code) {
  170. var linesCount = code.split(/\r\n|\r|\n/).length;
  171. var linesRatio = code.length / linesCount;
  172. var looksMinified = linesRatio > 2 * 1024;
  173. debug('Lines ratio is %d bytes per line', Math.round(linesRatio));
  174. debug(looksMinified ? 'It looks already minified' : 'It doesn\'t look minified');
  175. return looksMinified;
  176. }
  177. function entryTypeCanBeMinified(entry) {
  178. return entry.isJS || entry.isCSS || entry.isHTML;
  179. }
  180. return {
  181. minifyFile: minifyFile,
  182. minifyJs: minifyJs,
  183. minifyCss: minifyCss,
  184. minifyHtml: minifyHtml,
  185. gainIsEnough: gainIsEnough,
  186. entryTypeCanBeMinified: entryTypeCanBeMinified,
  187. isKnownAsMinified: isKnownAsMinified
  188. };
  189. };
  190. module.exports = new FileMinifier();