diff --git a/lib/tools/weightChecker/gzipCompressor.js b/lib/tools/weightChecker/gzipCompressor.js new file mode 100644 index 0000000..3ce8537 --- /dev/null +++ b/lib/tools/weightChecker/gzipCompressor.js @@ -0,0 +1,101 @@ +var debug = require('debug')('ylt:gzipCompressor'); + +var Q = require('q'); +var zlib = require('zlib'); + +var GzipCompressor = function() { + + function compressFile(entry) { + return gzipUncompressedFile(entry) + + .then(gzipOptimizedFile); + } + + // Gzip a file if it was not already gziped + function gzipUncompressedFile(entry) { + var deferred = Q.defer(); + + if (isCompressible(entry) && !entry.weightCheck.isCompressed) { + debug('Compression missing, trying to gzip file %s', entry.url); + + var uncompressedSize = entry.weightCheck.uncompressedSize; + + zlib.gzip(new Buffer(entry.weightCheck.body, 'utf8'), function(err, buffer) { + if (err) { + debug('Could not compress uncompressed file with gzip'); + debug(err); + + deferred.reject(err); + } else { + var compressedSize = buffer.length; + + if (gainIsEnough(uncompressedSize, compressedSize)) { + debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize); + + entry.weightCheck.afterCompression = compressedSize; + deferred.resolve(entry); + } else { + debug('Gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize); + } + } + }); + } else { + deferred.resolve(entry); + } + + return deferred.promise; + } + + // Gzip a file after minification or optimization if this step was successful + function gzipOptimizedFile(entry) { + var deferred = Q.defer(); + + if (isCompressible(entry) && !entry.weightCheck.isOptimized && !entry.weightCheck.isMinified) { + debug('Trying to gzip file after minification: %s', entry.url); + + var uncompressedSize = entry.weightCheck.optimized || entry.weightCheck.minified; + + zlib.gzip(new Buffer(entry.weightCheck.bodyAfterMinification, 'utf8'), function(err, buffer) { + if (err) { + debug('Could not compress minified file with gzip'); + debug(err); + + deferred.reject(err); + } else { + var compressedSize = buffer.length; + + if (gainIsEnough(uncompressedSize, compressedSize)) { + debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize); + + entry.weightCheck.afterOptimizationAndCompression = compressedSize; + deferred.resolve(entry); + } else { + debug('Gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize); + } + } + }); + } else { + deferred.resolve(entry); + } + + return deferred.promise; + } + + function isCompressible(entry) { + return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon; + } + + // The gain is estimated of enough value if it's over 1KB or over 20%, + // but it's ignored if is below 100 bytes + function gainIsEnough(oldWeight, newWeight) { + var gain = oldWeight - newWeight; + var ratio = gain / oldWeight; + return (gain > 2048 || (ratio > 0.2 && gain > 100)); + } + + return { + compressFile: compressFile + }; +}; + +module.exports = new GzipCompressor(); \ No newline at end of file diff --git a/lib/tools/weightChecker/weightChecker.js b/lib/tools/weightChecker/weightChecker.js index d90e629..f326ccf 100644 --- a/lib/tools/weightChecker/weightChecker.js +++ b/lib/tools/weightChecker/weightChecker.js @@ -14,6 +14,7 @@ var request = require('request'); var imageOptimizer = require('./imageOptimizer'); var fileMinifier = require('./fileMinifier'); +var gzipCompressor = require('./gzipCompressor'); var WeightChecker = function() { @@ -42,6 +43,8 @@ var WeightChecker = function() { .then(fileMinifier.minifyFile) + .then(gzipCompressor.compress) + .then(function(newEntry) { callback(null, newEntry); }) diff --git a/test/core/gzipCompressorTest.js b/test/core/gzipCompressorTest.js new file mode 100644 index 0000000..d69fcc7 --- /dev/null +++ b/test/core/gzipCompressorTest.js @@ -0,0 +1,586 @@ +var should = require('chai').should(); +var gzipCompressor = require('../../lib/tools/weightChecker/gzipCompressor'); +var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier'); +var fs = require('fs'); +var path = require('path'); + +describe('gzipCompressor', function() { + + var minifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/minified-script.js')); + var notMinifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js')); + var someTextFileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg')); + + + it('should gzip a JS file that was not gziped but was minified', function(done) { + var fileContent = minifiedJSContent; + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/minified-script.js', + status: 200, + isJS: true, + type: 'js', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip a JS file that was not gziped and not minified', function(done) { + /*jshint expr: true*/ + + var fileContent = notMinifiedJSContent; + var minifiedContent = minifiedJSContent; + + var fileSize = fileContent.length; + var minifiedSize = minifiedContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/unminified-script.js', + status: 200, + isJS: true, + type: 'js', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + bodyAfterMinification: minifiedContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: false, + minified: minifiedSize + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + delete newEntry.weightCheck.body; + delete newEntry.weightCheck.bodyAfterMinification; + console.log(newEntry.weightCheck); + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined; + newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(newEntry.weightCheck.afterCompression); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip a JS file that is gziped but not minified', function(done) { + /*jshint expr: true*/ + + var fileContent = notMinifiedJSContent; + var minifiedContent = minifiedJSContent; + var fileSize = 6436; + var gzipedSize = 2646; + var minifiedSize = 1954; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/unminified-script.js', + status: 200, + isJS: true, + type: 'js', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + bodyAfterMinification: minifiedContent.toString('utf8'), + totalWeight: gzipedSize + 200, + headersSize: 200, + bodySize: gzipedSize, + isCompressed: true, + uncompressedSize: fileSize, + isMinified: false, + minified: minifiedSize + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined; + newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(gzipedSize); + newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(minifiedSize); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should not gzip a JS file that was gziped and minified', function(done) { + /*jshint expr: true*/ + + var fileContent = notMinifiedJSContent; + var fileSize = 6436; + var gzipedSize = 2646; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/unminified-script.js', + status: 200, + isJS: true, + type: 'js', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: gzipedSize + 200, + headersSize: 200, + bodySize: gzipedSize, + isCompressed: true, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('minified'); + newEntry.weightCheck.should.not.have.a.property('bodyAfterMinification'); + newEntry.weightCheck.should.not.have.a.property('afterCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip a CSS file', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/unminified-stylesheet.css', + status: 200, + isCSS: true, + type: 'css', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip an HTML file', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/jquery-page.html', + status: 200, + isHTML: true, + type: 'html', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip an SVG file', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/svg-image.svg', + status: 200, + isImage: true, + isSVG: true, + type: 'image', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip an XML file', function(done) { + var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/someTextFile.xml', + status: 200, + isXML: true, + type: 'xml', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip a JSON file', function(done) { + var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/someTextFile.json', + status: 200, + isJSON: true, + type: 'json', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should gzip a TTF file', function(done) { + var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/someTextFile.ttf', + status: 200, + isWebFont: true, + isTTF: true, + type: 'webfont', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + + it('should gzip a favicon file', function(done) { + var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/someTextFile.ico', + status: 200, + isFavicon: true, + type: 'favicon', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isMinified: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should not gzip a JPEG file', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/jpeg-image.jpg', + status: 200, + isImage: true, + type: 'image', + contentType: 'image/jpeg', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isOptimized: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('afterCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + + it('should not gzip a PNG file', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/png-image.png', + status: 200, + isImage: true, + type: 'image', + contentType: 'image/png', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isOptimized: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('afterCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should not gzip a GIF file', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake gif, don't tell anyone... + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/gif-image.gif', + status: 200, + isImage: true, + type: 'image', + contentType: 'image/gif', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isOptimized: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('afterCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should not gzip a WEBP file', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake webp, don't tell anyone... + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/webp-image.webp', + status: 200, + isImage: true, + type: 'image', + contentType: 'image/webp', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize, + isOptimized: true + } + }; + + gzipCompressor.compressFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('afterCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + +}); diff --git a/test/www/minified-script.js b/test/www/minified-script.js new file mode 100644 index 0000000..7f3d520 --- /dev/null +++ b/test/www/minified-script.js @@ -0,0 +1,14 @@ +var timelineCtrl=angular.module("timelineCtrl",[]) +timelineCtrl.controller("TimelineCtrl",["$scope","$rootScope","$routeParams","$location","$timeout","Menu","Results","API",function(e,t,n,r,i,a,l,u){function o(){t.loadedResult&&t.loadedResult.runId===n.runId?(e.result=t.loadedResult,c()):l.get({runId:n.runId,exclude:"toolsResults"},function(n){t.loadedResult=n,e.result=n,c()})}function c(){s(),d(),f(),m(),i(p,100)}function s(){var t=r.hash(),n=null +0===t.indexOf("filter=")&&(n=t.substr(7)),e.warningsFilterOn=null!==n,e.warningsFilters={queryWithoutResults:null===n||"queryWithoutResults"===n,jQueryCallOnEmptyObject:null===n||"jQueryCallOnEmptyObject"===n,eventNotDelegated:null===n||"eventNotDelegated"===n,jsError:null===n||"jsError"===n}}function d(){var t=e.result.rules.jsCount.offendersObj.list +e.scripts=[],t.forEach(function(t){var n=t.file +n.length>100&&(n=n.substr(0,98)+"...") +var r={fullPath:t.file,shortPath:n} +e.scripts.push(r)})}function f(){var t=e.result.javascriptExecutionTree.children||[],n=t[t.length-1] +e.endTime=n.data.timestamp+(n.data.time||0),e.executionTree=[],t.forEach(function(t){if(e.selectedScript){if(t.data.backtrace&&-1===t.data.backtrace.indexOf(e.selectedScript.fullPath+":"))return +if("jQuery loaded"===t.data.type||"jQuery version change"===t.data.type)return}e.executionTree.push(t)})}function m(){var t=199 +e.timelineIntervalDuration=e.endTime/t +var n=Array.apply(null,Array(e.endTime+1)).map(Number.prototype.valueOf,0) +e.executionTree.forEach(function(e){if(void 0!==e.data.time)for(var t=Math.min(e.data.time,100)||1,r=e.data.timestamp,i=e.data.timestamp+t;i>r;r++)n[r]|=1}),e.timeline=Array.apply(null,Array(t+1)).map(Number.prototype.valueOf,0),n.forEach(function(t,n){1===t&&(e.timeline[Math.floor(n/e.timelineIntervalDuration)]+=1)}),e.timelineMax=Math.max.apply(Math,e.timeline)}function p(){e.profilerData=e.executionTree}e.runId=n.runId,e.Menu=a.setCurrentPage("timeline",e.runId),e.changeScript=function(){f(),m(),p()},e.findLineIndexByTimestamp=function(t){for(var n=0,r=0;r0)break}return n},e.backToDashboard=function(){r.path("/result/"+e.runId)},e.testAgain=function(){u.relaunchTest(e.result)},o()}]),timelineCtrl.directive("scrollOnClick",["$animate","$timeout",function(e,t){return{restrict:"A",link:function(n,r,i){r.on("click",function(){var r=n.findLineIndexByTimestamp(i.scrollOnClick),a=angular.element(document.getElementById("line_"+r)) +a.addClass("highlight"),t(function(){e.removeClass(a,"highlight"),n.$digest()},50),window.scrollTo(0,a[0].offsetTop)})}}}])