File minification

This commit is contained in:
Gaël Métais
2015-06-16 20:33:19 +02:00
parent 41bc676b12
commit c1bae20f01
12 changed files with 1456 additions and 37 deletions
@@ -865,7 +865,7 @@
var kilo = bytes / 1024;
if (kilo < 1) {
return bytes + ' Bytes';
return bytes + ' bytes';
}
if (kilo < 100) {
+22 -2
View File
@@ -137,9 +137,9 @@
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
<div ng-if="policyName === 'requests' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
<url-link url="offender.file" max-length="100"></url-link>
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
<span ng-if="offender.size || offender.size === 0">({{offender.size}} KB)</span>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
@@ -230,6 +230,26 @@
</div>
</div>
<div ng-if="policyName === 'fileMinification'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Minified</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="60"></url-link>
</div>
<div>{{file.original | bytes}}</div>
<div>{{file.minified | bytes}}</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'DOMaccesses'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
+11 -1
View File
@@ -872,6 +872,16 @@ var policies = {
"hasOffenders": true,
"unit": 'bytes'
},
"fileMinification": {
"tool": "weightChecker",
"label": "File minification",
"message": "<p>This is the weight that could be saved if all text resources were correctly minified.</p><p>The tools in use here are <b>UglifyJS</b>, <b>clean-css</b> and <b>HTMLMinifier</b>.</p><p>The gains of minification are generally small, but the impact can be high when these text files are loaded on the critical path.</p>",
"isOkThreshold": 20480,
"isBadThreshold": 81920,
"isAbnormalThreshold": 153600,
"hasOffenders": true,
"unit": 'bytes'
},
"requests": {
"tool": "phantomas",
"label": "Total requests number",
@@ -888,7 +898,7 @@ var policies = {
.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
}).sort(function(a, b) {
return b.size - a.size;
return (b.file < a.file) ? 1 : (b.file > a.file) ? -1 : 0;
})
};
}
+3 -1
View File
@@ -73,7 +73,9 @@
"label": "Page weight",
"policies": {
"totalWeight": 5,
"imageOptimization": 3
"imageOptimization": 2,
"fileMinification": 1,
"assetsNotGzipped": 2
}
},
"requests": {
+195
View File
@@ -0,0 +1,195 @@
var debug = require('debug')('ylt:fileMinifier');
var Q = require('q');
var UglifyJS = require('uglify-js');
var CleanCSS = require('clean-css');
var Minimize = require('minimize');
var FileMinifier = function() {
function minifyFile(entry) {
var deferred = Q.defer();
if (!entry.weightCheck || !entry.weightCheck.body) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
debug('Let\'s try to optimize %s', entry.url);
debug('Current file size is %d', fileSize);
if (entry.isJS) {
debug('File is a JS');
// Starting softly with a lossless compression
return minifyJs(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.length;
debug('JS minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.minified = newFileSize;
entry.weightCheck.isMinified = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else if (entry.isCSS) {
debug('File is a CSS');
// Starting softly with a lossless compression
return minifyCss(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.length;
debug('CSS minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.minified = newFileSize;
entry.weightCheck.isMinified = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else if (entry.isHTML) {
debug('File is an HTML');
// Starting softly with a lossless compression
return minifyHtml(entry.weightCheck.body)
.then(function(newFile) {
console.log('KKKKKKKKKKKK');
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.length;
debug('HTML minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.minified = newFileSize;
entry.weightCheck.isMinified = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
} else {
console.log('OOOO old file size: ' + fileSize);
console.log('OOOO new file size: ' + newFileSize);
console.log(entry.weightCheck);
}
return entry;
})
.fail(function(err) {
console.log('LLLLLLLLLLLLLLL');
console.log(err);
return entry;
});
} else {
debug('File type %s is not an (optimizable) image', entry.contentType);
deferred.resolve(entry);
}
return deferred.promise;
}
// The gain is estimated of enough value if it's over 2KB 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 > 2096 || (ratio > 0.2 && gain > 400));
}
// Uglify
function minifyJs(body) {
var deferred = Q.defer();
try {
var result = UglifyJS.minify(body, {fromString: true});
deferred.resolve(result.code);
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
// Clear-css
function minifyCss(body) {
var deferred = Q.defer();
try {
var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
deferred.resolve(result.styles);
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
// HTMLMinifier
function minifyHtml(body) {
var deferred = Q.defer();
var minimize = new Minimize({
empty: true, // KEEP empty attributes
conditionals: true, // KEEP conditional internet explorer comments
spare: true // KEEP redundant attributes
});
minimize.parse(body, function (error, data) {
if (error) {
deferred.reject(error);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
}
return {
minifyFile: minifyFile,
minifyJs: minifyJs,
minifyCss: minifyCss,
minifyHtml: minifyHtml,
gainIsEnough: gainIsEnough
};
};
module.exports = new FileMinifier();
+3 -4
View File
@@ -7,18 +7,18 @@ var jpegoptim = require('imagemin-jpegoptim');
var ImageOptimizer = function() {
var MAX_JPEG_QUALITY = 85;
var OPTIPNG_COMPRESSION_LEVEL = 2;
var OPTIPNG_COMPRESSION_LEVEL = 1;
function optimizeImage(entry) {
var deferred = Q.defer();
if (!entry.weightCheck.body) {
if (!entry.weightCheck || !entry.weightCheck.body) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.bodySize;
var fileSize = entry.weightCheck.uncompressedSize;
debug('Let\'s try to optimize %s', entry.url);
debug('Current file size is %d', fileSize);
@@ -227,7 +227,6 @@ var ImageOptimizer = function() {
}
return {
recompressIfImage: optimizeImage,
optimizeImage: optimizeImage,
compressJpegLosslessly: compressJpegLosslessly,
compressJpegLossly: compressJpegLossly,
+68 -12
View File
@@ -13,6 +13,7 @@ var async = require('async');
var request = require('request');
var imageOptimizer = require('./imageOptimizer');
var fileMinifier = require('./fileMinifier');
var WeightChecker = function() {
@@ -23,6 +24,8 @@ var WeightChecker = function() {
// 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);
@@ -35,7 +38,9 @@ var WeightChecker = function() {
redownloadEntry(entry)
.then(imageOptimizer.recompressIfImage)
.then(imageOptimizer.optimizeImage)
.then(fileMinifier.minifyFile)
.then(function(newEntry) {
callback(null, newEntry);
@@ -49,11 +54,20 @@ var WeightChecker = function() {
// 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);
// Remove unwanted requests (redirections, about:blank)
results = results.filter(function(result) {
return (result !== null && result.weightCheck && result.weightCheck.bodySize > 0);
});
var metrics = {};
var offenders = {};
@@ -67,6 +81,11 @@ var WeightChecker = function() {
offenders.imageOptimization = listImageNotOptimized(results);
metrics.imageOptimization = offenders.imageOptimization.totalGain;
// File minification
offenders.fileMinification = listFilesNotMinified(results);
metrics.fileMinification = offenders.fileMinification.totalGain;
data.toolsResults.weightChecker = {
metrics: metrics,
@@ -144,14 +163,14 @@ var WeightChecker = function() {
};
requests.forEach(function(req) {
if (req.weightCheck.bodySize && req.weightCheck.isOptimized === false) {
var gain = req.weightCheck.bodySize - req.weightCheck.optimized;
if (req.weightCheck.uncompressedSize && req.weightCheck.isOptimized === false) {
var gain = req.weightCheck.uncompressedSize - req.weightCheck.optimized;
results.totalGain += gain;
results.images.push({
url: req.url,
original: req.weightCheck.bodySize,
original: req.weightCheck.uncompressedSize,
optimized: req.weightCheck.optimized,
lossless: req.weightCheck.lossless,
lossy: req.weightCheck.lossy,
@@ -164,10 +183,35 @@ var WeightChecker = function() {
}
function listFilesNotMinified(requests) {
var results = {
totalGain: 0,
files: []
};
requests.forEach(function(req) {
if (req.weightCheck.uncompressedSize && req.weightCheck.isMinified === false) {
var gain = req.weightCheck.uncompressedSize - req.weightCheck.minified;
results.totalGain += gain;
results.files.push({
url: req.url,
original: req.weightCheck.uncompressedSize,
minified: req.weightCheck.minified,
gain: gain
});
}
});
return results;
}
function redownloadEntry(entry) {
var deferred = Q.defer();
function onError(message) {
function downloadError(message) {
debug('Could not download %s Error: %s', entry.url, message);
entry.weightCheck = {
message: message
@@ -175,18 +219,32 @@ var WeightChecker = function() {
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') {
onError('only downloading GET');
notDownloadableFile('only downloading GET');
return deferred.promise;
}
if (entry.status !== 200) {
onError('only downloading requests with status code 200');
unwantedFile('only downloading requests with status code 200');
return deferred.promise;
}
if (entry.url === 'about:blank') {
onError('not downloading about:blank');
unwantedFile('not downloading about:blank');
return deferred.promise;
}
@@ -202,15 +260,14 @@ var WeightChecker = function() {
url: entry.url,
headers: reqHeaders,
timeout: REQUEST_TIMEOUT
//encoding: (entry.contentType === 'image/jpeg' || entry.contentType === 'image/png') ? 'binary' : null
};
download(requestOptions, entry.contentType, function(error, result) {
if (error) {
if (error.code === 'ETIMEDOUT') {
onError('timeout after ' + REQUEST_TIMEOUT + 'ms');
downloadError('timeout after ' + REQUEST_TIMEOUT + 'ms');
} else {
onError('error while downloading: ' + error.code);
downloadError('error while downloading: ' + error.code);
}
return;
}
@@ -332,7 +389,6 @@ var WeightChecker = function() {
});
}
return {
recheckAllFiles: recheckAllFiles,
listRequestWeight: listRequestWeight,
+15 -12
View File
@@ -11,21 +11,24 @@
},
"main": "./lib/index.js",
"dependencies": {
"async": "~1.0.0",
"body-parser": "~1.12.4",
"compression": "~1.4.4",
"cors": "^2.6.0",
"debug": "~2.2.0",
"express": "~4.12.4",
"imagemin": "~3.2.0",
"imagemin-jpegoptim": "~4.0.0",
"async": "1.0.0",
"body-parser": "1.12.4",
"clean-css": "3.3.0",
"compression": "1.4.4",
"cors": "2.6.0",
"debug": "2.2.0",
"express": "4.12.4",
"imagemin": "3.2.0",
"imagemin-jpegoptim": "4.0.0",
"lwip": "0.0.6",
"meow": "^3.1.0",
"meow": "3.1.0",
"minimize": "1.4.1",
"phantomas": "1.10.2",
"ps-node": "0.0.4",
"q": "~1.4.1",
"rimraf": "~2.3.4",
"temporary": "0.0.8"
"q": "1.4.1",
"rimraf": "2.3.4",
"temporary": "0.0.8",
"uglify-js": "2.4.23"
},
"devDependencies": {
"chai": "^2.3.0",
+214
View File
@@ -0,0 +1,214 @@
var should = require('chai').should();
var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
var fs = require('fs');
var path = require('path');
describe('fileMinifier', function() {
it('should minify a JS file with minifyJs', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
var fileSize = fileContent.length;
fileMinifier.minifyJs(fileContent.toString()).then(function(newFile) {
var newFileSize = newFile.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should minify a JS file with minifyFile', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/unminified-script.js',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('isMinified').that.equals(false);
newEntry.weightCheck.should.have.a.property('minified').that.is.below(fileSize);
done();
})
.fail(function(err) {
done(err);
});
});
it('should fail minifying an already minified JS', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery1.8.3.js'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/jquery1.8.3.js',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('isMinified');
newEntry.weightCheck.should.not.have.a.property('minified');
done();
})
.fail(function(err) {
done(err);
});
});
it('should fail minifying a JS with syntax errors', 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',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('isMinified');
newEntry.weightCheck.should.not.have.a.property('minified');
done();
})
.fail(function(err) {
done(err);
});
});
it('should minify a CSS file with clean-css', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
var fileSize = fileContent.length;
fileMinifier.minifyCss(fileContent.toString()).then(function(newFile) {
var newFileSize = newFile.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should minify a CSS file with minifyFile', 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',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isCSS: true,
type: 'css',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('isMinified').that.equals(false);
newEntry.weightCheck.should.have.a.property('minified').that.is.below(fileSize);
done();
})
.fail(function(err) {
done(err);
});
});
it('should minify an HTML file with ', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
var fileSize = fileContent.length;
fileMinifier.minifyHtml(fileContent.toString()).then(function(newFile) {
var newFileSize = newFile.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
});
+32 -4
View File
@@ -57,6 +57,30 @@ describe('weightChecker', function() {
type: 'image',
contentType: 'image/svg+xml'
},
{
method: 'GET',
url: 'http://localhost:8388/unminified-script.js',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*'
},
status: 200,
isJS: true,
type: 'js'
},
{
method: 'GET',
url: 'http://localhost:8388/unminified-stylesheet.css',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*'
},
status: 200,
isCSS: true,
type: 'css'
},
{
method: 'GET',
url: 'about:blank',
@@ -92,14 +116,19 @@ describe('weightChecker', function() {
data.toolsResults.weightChecker.offenders.should.have.a.property('totalWeight');
data.toolsResults.weightChecker.offenders.totalWeight.totalWeight.should.be.above(0);
data.toolsResults.weightChecker.offenders.totalWeight.byType.html.requests.length.should.equal(1);
data.toolsResults.weightChecker.offenders.totalWeight.byType.js.requests.length.should.equal(1);
data.toolsResults.weightChecker.offenders.totalWeight.byType.js.requests.length.should.equal(2);
data.toolsResults.weightChecker.offenders.totalWeight.byType.css.requests.length.should.equal(1);
data.toolsResults.weightChecker.offenders.totalWeight.byType.image.requests.length.should.equal(2);
data.toolsResults.weightChecker.offenders.totalWeight.byType.other.requests.length.should.equal(1);
data.toolsResults.weightChecker.offenders.totalWeight.byType.other.requests.length.should.equal(0);
data.toolsResults.weightChecker.offenders.should.have.a.property('imageOptimization');
data.toolsResults.weightChecker.offenders.imageOptimization.totalGain.should.be.above(0);
data.toolsResults.weightChecker.offenders.imageOptimization.images.length.should.equal(2);
data.toolsResults.weightChecker.offenders.should.have.a.property('fileMinification');
data.toolsResults.weightChecker.offenders.fileMinification.totalGain.should.be.above(0);
data.toolsResults.weightChecker.offenders.fileMinification.files.length.should.equal(2);
done();
})
@@ -222,8 +251,7 @@ describe('weightChecker', function() {
weightChecker.redownloadEntry(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('message').that.equals('only downloading requests with status code 200');
newEntry.should.not.have.a.property('weightCheck');
done();
})
+190
View File
@@ -0,0 +1,190 @@
var timelineCtrl = angular.module('timelineCtrl', []);
timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
render();
});
} else {
$scope.result = $rootScope.loadedResult;
render();
}
}
function render() {
initFilters();
initScriptFiltering();
initExecutionTree();
initTimeline();
$timeout(initProfiler, 100);
}
function initFilters() {
var hash = $location.hash();
var filter = null;
if (hash.indexOf('filter=') === 0) {
filter = hash.substr(7);
}
$scope.warningsFilterOn = (filter !== null);
$scope.warningsFilters = {
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
jsError: (filter === null || filter === 'jsError')
};
}
function initScriptFiltering() {
var offenders = $scope.result.rules.jsCount.offendersObj.list;
$scope.scripts = [];
offenders.forEach(function(script) {
var filePath = script.file;
if (filePath.length > 100) {
filePath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: script.file,
shortPath: filePath
};
$scope.scripts.push(scriptObj);
});
}
function initExecutionTree() {
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
// Detect the last event of all (before filtering) and read time
var lastEvent = originalExecutions[originalExecutions.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
// Filter
$scope.executionTree = [];
originalExecutions.forEach(function(node) {
// Filter by script (if enabled)
if ($scope.selectedScript) {
if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) {
return;
}
if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") {
return;
}
}
$scope.executionTree.push(node);
});
}
function initTimeline() {
// Split the timeline into 200 intervals
var numberOfIntervals = 199;
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
// Pre-fill array of as many elements as there are milleseconds
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
// Create the milliseconds array from the execution tree
$scope.executionTree.forEach(function(node) {
if (node.data.time !== undefined) {
// Ignore artefacts (durations > 100ms)
var time = Math.min(node.data.time, 100) || 1;
for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
millisecondsArray[i] |= 1;
}
}
});
// Pre-fill array of 200 elements
$scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
// Create the timeline from the milliseconds array
millisecondsArray.forEach(function(value, timestamp) {
if (value === 1) {
$scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
}
});
// Get the maximum value of the array (needed for display)
$scope.timelineMax = Math.max.apply(Math, $scope.timeline);
}
function initProfiler() {
$scope.profilerData = $scope.executionTree;
}
$scope.changeScript = function() {
initExecutionTree();
initTimeline();
initProfiler();
};
$scope.findLineIndexByTimestamp = function(timestamp) {
var lineIndex = 0;
for (var i = 0; i < $scope.executionTree.length; i ++) {
var delta = $scope.executionTree[i].data.timestamp - timestamp;
if (delta < $scope.timelineIntervalDuration) {
lineIndex = i;
}
if (delta > 0) {
break;
}
}
return lineIndex;
};
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.relaunchTest($scope.result);
};
loadResults();
}]);
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
// When the user clicks on the timeline, find the right profiler line and scroll to it
element.on('click', function() {
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
// Animate the background color to "flash" the row
lineElement.addClass('highlight');
$timeout(function() {
$animate.removeClass(lineElement, 'highlight');
scope.$digest();
}, 50);
window.scrollTo(0, lineElement[0].offsetTop);
});
}
};
}]);
+702
View File
@@ -0,0 +1,702 @@
/* Timeline colors, related to Window Performances */
.execution {
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
}
.selectScript select {
max-width: 30em;
}
.selectScript.empty {
font-size: 0.8em;
}
.selectScript.empty select {
width: 10em;
}
.timeline {
margin: 2em 0 5em;
}
.timeline .chart {
position: relative;
width: 100%;
border-bottom: 1px solid #000;
}
.timeline .startTime,
.timeline .endTime {
position: absolute;
bottom: 0.5em;
font-size: 0.8em;
}
.timeline .startTime {
left: 0em;
}
.timeline .endTime {
right: 0em;
}
.timeline .chartPoints {
display: table;
height: 100px;
width: 99%;
margin: 0 auto;
}
.timeline .interval {
display: table-cell;
position: relative;
height: 100px;
width: 0.5%;
}
.timeline .interval .color {
position: absolute;
bottom: 0;
width: 100%;
}
.timeline .interval .color.clickable {
cursor: pointer;
}
.timeline div.interval:hover {
background: #9C4274;
}
.timeline .interval:hover .color {
background: #F04DA7;
}
.timeline .domComplete.interval {
background: #ede3ff;
}
.timeline .domComplete .color {
background: #c2a3ff;
}
.timeline .domContentLoadedEnd.interval {
background: #d8f0f0;
}
.timeline .domContentLoadedEnd .color {
background: #7ecccc;
}
.timeline .domContentLoaded.interval {
background: #e0ffd1;
}
.timeline .domContentLoaded .color {
background: #a7e846;
}
.timeline .domInteractive.interval {
background: #fffccc;
}
.timeline .domInteractive .color {
background: #ffe433;
}
.timeline .domCreation.interval {
background: #ffe0cc;
}
.timeline .domCreation .color {
background: #ff6600;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
display: none;
width: auto;
padding: 0.5em 1em;
top: -1.5em;
right: 1em;
}
.timeline .interval:hover .tooltip {
display: block;
}
.timeline .legend {
display: table;
width: 100%;
margin-top: 1em;
}
.timeline .legend > div {
display: table-row;
}
.timeline .legend > div > div {
position: relative;
display: table-cell;
margin-top: 1em;
}
.timeline .titles {
font-weight: bold;
}
.timeline .titles > div {
padding: 0 1em 0 2em;
}
.timeline .tips {
font-size: 0.7em;
}
.timeline .tips > div {
padding: 1em 1em 0 0;
}
.timeline .legend .color {
display: block;
position: absolute;
left: 0;
height: 1.5em;
width: 1.5em;
border-radius: 0.2em;
}
.filters {
margin: 1em auto;
padding: 0.5em;
min-width: 30em;
width: 30%;
border: 1px dotted #aaa;
text-align: left;
}
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.table > div.jsError > .type,
.table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
}
.table > div.windowPerformance > div,
.table > div.windowPerformance > div.startTime {
background: #EBD8E2;
}
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div.highlight > div.startTime {
background-color: #C0F090;
}
.table > div.highlight-remove {
transition: 3s;
}
.table > div.highlight-remove > div.startTime {
transition: background-color 3s ease-in;
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space: nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
}
.table .child > .child {
margin-left: 1em;
}
.table .child:before {
content: "↳";
}
.table .child .childArgs {
display: none;
}
.table .child span {
position: relative;
}
.table .child span:hover {
background: #EBD8E2;
}
.table .child span:hover div {
display: inline-block;
}
.table .child span:hover .childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 2;
}
.table .showingDetails .child span:hover {
background: inherit;
}
.table .showingDetails .child span:hover .childArgs {
display: none;
}
.table > div > .value {
width: 70%;
word-break: break-all;
}
.table > div > .details {
position: relative;
}
.table .details .icon-question {
color: #f1c40f;
cursor: pointer;
}
.table .icon-warning {
display: inline-block;
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
width: 45em;
min-height: 1em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
}
.detailsOverlay .advice {
color: #e74c3c;
font-weight: bold;
}
.detailsOverlay .trace {
word-break: break-all;
}
.table > div > .duration,
.table > div > .startTime {
text-align: center;
white-space: nowrap;
}
.table > div > .startTime.domComplete {
background: #ede3ff;
}
.table > div > .startTime.domContentLoadedEnd {
background: #d8f0f0;
}
.table > div > .startTime.domContentLoaded {
background: #e0ffd1;
}
.table > div > .startTime.domInteractive {
background: #fffccc;
}
.table > div > .startTime.domCreation {
background: #ffe0cc;
}
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.queryWithoutResultsFilterOn > div {
display: none;
}
.queryWithoutResultsFilterOn > div.queryWithoutResults {
display: table-row;
}
.jQueryCallOnEmptyObjectFilterOn > div {
display: none;
}
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
display: table-row;
}
.eventNotDelegatedFilterOn > div {
display: none;
}
.eventNotDelegatedFilterOn > div.eventNotDelegated {
display: table-row;
}
.jsErrorFilterOn > div {
display: none;
}
.jsErrorFilterOn > div.jsError {
display: table-row;
}
.testedUrl {
color: inherit;
}
.summary {
text-align: center;
}
.summary .globalScore {
display: table;
width: 60%;
margin: 3em auto;
}
.summary .globalScore > div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.summary .globalScore .globalGrade {
margin: 0.5 auto;
width: 2.5em;
height: 2.5em;
line-height: 2.5em;
border-radius: 0.5em;
font-size: 3em;
font-weight: bold;
vertical-align: middle;
}
.summary .globalScore .on100 {
font-size: 1.2em;
font-weight: bold;
margin: 0.5em 0 1em;
}
.summary .globalScore .screenshotWrapper:hover {
opacity: 0.75;
}
.summary .globalScore .screenshotWrapper:hover:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
.summary .globalScore .screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.summary .globalScore .screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
.summary .notations {
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
}
.summary .notations > div {
display: table-row;
}
.summary .notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
}
.summary .notations .A.categoryScore,
.summary .notations .B.categoryScore,
.summary .notations .C.categoryScore,
.summary .notations .D.categoryScore,
.summary .notations .E.categoryScore,
.summary .notations .F.categoryScore,
.summary .notations .NA.categoryScore {
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.summary .notations .grade .A,
.summary .notations .grade .B,
.summary .notations .grade .C,
.summary .notations .grade .D,
.summary .notations .grade .E,
.summary .notations .grade .F,
.summary .notations .grade .NA {
width: 1em;
height: 1em;
font-size: 1em;
color: transparent;
margin: 0 auto;
border-radius: 0.5em;
}
.summary .notations .criteria .table {
width: 100%;
}
.summary .notations .criteria .table > div:hover > div {
background: #EBD8E2;
cursor: pointer;
}
.summary .notations .criteria .table > div:hover > div.info {
background: #FFF;
}
.summary .notations .criteria .table > div:hover > div.info .icon-question {
color: #EBD8E2;
}
.summary .notations .criteria .grade {
width: 10%;
padding-left: 0.5em;
padding-right: 0.5em;
vertical-align: middle;
}
.summary .notations .criteria .label {
width: 70%;
}
.summary .notations .criteria .result {
width: 18%;
font-weight: bold;
white-space: nowrap;
text-align: center;
vertical-align: middle;
}
.summary .notations .warning .label,
.summary .notations .warning .result,
.summary .notations .icon-warning {
color: #FF1919;
}
.summary .notations .criteria .info {
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
.summary .notations .criteria .icon-question {
color: transparent;
}
.summary .fromShare {
margin-bottom: 3em;
}
.summary .apiTip {
font-size: 0.8em;
margin-bottom: 4em;
color: #413;
}
.summary .apiTip a {
color: inherit;
}
.summary .tweet .tweetText {
color: #413;
background: #F2F2F2;
border: none;
width: 25em;
padding: 0.4em;
border-radius: 0.5em;
box-shadow: 0.05em 0.1em 0 0 #999;
}
.summary .tweet .tweetButton,
.summary .tweet .linkedinButton {
color: #413;
background: #F2F2F2;
margin-right: 0;
}
.summary .tweet .tweetButton:hover,
.summary .tweet .linkedinButton:hover {
color: #F2F2F2;
background: #e74c3c;
}
.summary .tweet input {
font-size: 0.9em;
}
.promess {
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #ffa319;
color: #fff;
}
.launchBtn:focus {
background: #e74c3c;
}
.launchBtn.disabled {
background: #deaca6;
}
.launchBtn.disabled:focus {
color: #ddd;
}
.settings {
width: 50%;
margin: 0 auto;
}
.settings input,
.settings select {
font-size: 1em;
}
.settings input[type=text] {
width: 100%;
min-width: 4em;
}
.device {
margin-top: 3em;
}
.device .item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
}
.device .item.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
.device .item:hover {
color: #ffa319;
}
.device .item > div {
margin: 0.2em 0 0.1em;
font-size: 3em;
}
.settingsTooltip {
position: relative;
}
.settingsTooltip span {
font-size: 0.8em;
vertical-align: text-top;
}
.settingsTooltip div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
.settingsTooltip:hover div {
display: block;
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
}
.showAdvanced:hover {
color: #ffa319;
}
.currentSettings {
font-size: 0.9em;
}
.currentSettings span {
color: #ffa319;
}
.currentSettings span:after {
color: #FFF;
content: ",";
}
.currentSettings span:last-child:after {
content: "";
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
}
.advanced > div {
display: table-row;
}
.advanced > div > div {
display: table-cell;
width: 75%;
}
.advanced > div > div.label {
width: 25%;
white-space: nowrap;
}
.advanced .subTable {
display: table;
border-spacing: 0;
width: 100%;
}
.advanced .subTable > div {
display: table-row;
}
.advanced .subTable > div > div {
display: table-cell;
padding: 0 0 0.75em;
}
.features {
display: table;
width: 50%;
margin: 6em auto 0;
font-size: 0.9em;
color: #413;
}
.features > div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
input[type=submit],
input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
color: #ddd;
}
input[type=submit].clicked {
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}