Compare commits

...
26 Commits
Author SHA1 Message Date
Gaël Métais f2e77a5c3a v1.9.1 2015-11-27 09:52:28 +01:00
Gaël Métais 23f4f9bd0b v1.9.1 2015-11-27 09:52:10 +01:00
Gaël Métais e8eb6964db Missing legend on breakpoint graph 2015-11-27 09:50:08 +01:00
Gaël Métais 9643c5bfcd Merge pull request #128 from gmetais/develop
Pushing to production soon
2015-11-26 18:50:33 +01:00
Gaël Métais 56ebe6bb8b Remove some chapters in doc 2015-11-26 18:44:48 +01:00
Gaël Métais 5f7c2f07b8 Adjust new cssMobileFirst rule 2015-11-26 18:44:12 +01:00
Gaël Métais bbe965e7a3 Merge pull request #127 from gmetais/dependencies
Update dependencies
2015-11-26 17:50:58 +01:00
Gaël Métais ebd03c7b1a Update dependencies 2015-11-26 17:41:22 +01:00
Gaël Métais 0d02a57600 Merge pull request #126 from gmetais/mediaQueries
Analyze media queries
2015-11-26 15:22:58 +01:00
Gaël Métais 394d3a5b25 Two new media queries related rules 2015-11-26 14:57:47 +01:00
Gaël Métais 4f9589fb6b Fix rare Cannot read property '1' of null error 2015-11-26 09:55:21 +01:00
Gaël Métais 048e3396de Merge pull request #122 from gmetais/develop
v1.8.2
2015-11-06 16:48:25 +01:00
Gaël Métais e3d378a4ba v1.8.2 2015-11-06 16:23:13 +01:00
Gaël Métais 91f8d9c2b0 Merge PR into develop: Change default user agent 2015-11-06 15:26:56 +01:00
Martin Smeeckaert 2285c896d2 change default user agent 2015-11-06 11:51:52 +01:00
Gaël Métais 9ff24918e2 Merge pull request #120 from gmetais/develop
v1.8.1
2015-11-02 19:18:53 +01:00
Gaël Métais e78e794240 Fix jsHint 2015-11-02 19:07:35 +01:00
Gaël Métais 0eb359542b Temporary fix for relative lazyloadable images src 2015-11-02 18:56:33 +01:00
Gaël Métais c54cf5a2ba Relaunch run on webkit1 if webkit2 fails 2015-10-28 22:55:52 +01:00
Gaël Métais 6f7f05b456 Small fix for old prefixes offenders 2015-10-28 22:09:26 +01:00
Gaël Métais da6a8834dd Merge pull request #119 from gmetais/old-prefixes
Merged
2015-10-28 21:28:13 +01:00
Gaël Métais 7ed16d63f3 Merge pull request #118 from gmetais/phantomas1.13
Merged
2015-10-28 21:26:02 +01:00
Gaël Métais d8730cc67b Fix tests after phantomas v1.13 2015-10-28 20:07:02 +01:00
Gaël Métais b67efe0cac Update dependencies 2015-10-28 20:05:43 +01:00
Gaël Métais 835cb2f90f Improve Old prefixes offenders readability 2015-10-28 12:04:00 +01:00
Gaël Métais f75e99f703 Fix bug where scrollExecutionTree is empty 2015-10-23 17:31:00 +02:00
17 changed files with 600 additions and 127 deletions
+4 -10
View File
@@ -1,6 +1,6 @@
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Easy to launch, easy to understand, in-depth details.
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Free, easy to launch, easy to understand, in-depth details.
#
@@ -75,16 +75,10 @@ You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel bet
If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
## Help needed!
There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
## License
Please read the [license](LICENSE). Icons are the property of [IcoMoon.io](https://icomoon.io/)
## Author
Gaël Métais. I'm a webperf freelance based in Paris. If you understand French, you can visit [my website](http://www.gaelmetais.com).
Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
If you understand French, you can visit [my website](http://www.gaelmetais.com) (will be soon in English too).
## Contributors
+6 -1
View File
@@ -101,6 +101,7 @@
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
cursor: pointer;
}
.offendersTable .offenderButton.opens,
.value .offenderButton.opens {
@@ -157,6 +158,10 @@
display: block;
background: #ffe0cc;
}
.offendersTable .smallerOffenders,
.value .smallerOffenders {
font-size: 0.9em;
}
.offendersHtml {
display: inline-block;
}
@@ -174,7 +179,7 @@
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
background-size: 1em 1em;
background-position: 0 0, 0.5em 0.5em;
}
+15 -15
View File
@@ -62,34 +62,34 @@
background: #F04DA7;
}
.timeline .domComplete.interval {
background: #ede3ff;
background: #EDE3FF;
}
.timeline .domComplete .color {
background: #c2a3ff;
background: #C2A3FF;
}
.timeline .domContentLoadedEnd.interval {
background: #d8f0f0;
background: #D8F0F0;
}
.timeline .domContentLoadedEnd .color {
background: #7ecccc;
background: #7ECCCC;
}
.timeline .domContentLoaded.interval {
background: #e0ffd1;
background: #E0FFD1;
}
.timeline .domContentLoaded .color {
background: #a7e846;
background: #A7E846;
}
.timeline .domInteractive.interval {
background: #fffccc;
background: #FFFCCC;
}
.timeline .domInteractive .color {
background: #ffe433;
background: #FFE433;
}
.timeline .domCreation.interval {
background: #ffe0cc;
background: #FFE0CC;
}
.timeline .domCreation .color {
background: #ff6600;
background: #FF6600;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
@@ -294,19 +294,19 @@
white-space: nowrap;
}
.table > div > .startTime.domComplete {
background: #ede3ff;
background: #EDE3FF;
}
.table > div > .startTime.domContentLoadedEnd {
background: #d8f0f0;
background: #D8F0F0;
}
.table > div > .startTime.domContentLoaded {
background: #e0ffd1;
background: #E0FFD1;
}
.table > div > .startTime.domInteractive {
background: #fffccc;
background: #FFFCCC;
}
.table > div > .startTime.domCreation {
background: #ffe0cc;
background: #FFE0CC;
}
.execution .icon-warning {
color: #e74c3c;
+49
View File
@@ -48,6 +48,55 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
tooltipTemplate: '<%=label%>: <%=value%> KB'
};
}
// Init "Breakpoints" chart
if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
// Seek for the biggest breakpoint
var max = 0;
$scope.rule.offendersObj.forEach(function(offender) {
if (offender.pixels > max) {
max = offender.pixels;
}
});
max = Math.max(max + 100, 1400);
// We group offenders 10px by 10px
var GROUP_SIZE = 20;
// Generate an empty array of values
$scope.breakpointsLabels = [];
$scope.breakpointsData = [[]];
for (var i = 0; i <= max / GROUP_SIZE; i++) {
$scope.breakpointsLabels[i] = '';
$scope.breakpointsData[0][i] = 0;
}
// Fill it with results
$scope.rule.offendersObj.forEach(function(offender) {
var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
if ($scope.breakpointsLabels[group] !== '') {
$scope.breakpointsLabels[group] += '/';
}
$scope.breakpointsLabels[group] += offender.breakpoint;
$scope.breakpointsData[0][group] += offender.count;
});
$scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
$scope.breakpointsColours = ['#9c4274'];
$scope.breakpointsOptions = {
scaleShowGridLines: false,
barShowStroke: false,
showTooltips: false,
pointDot: false,
responsive: true,
maintainAspectRatio: true,
strokeColor: 'rgba(20, 200, 20, 1)',
scaleFontSize: 9
};
}
}
$scope.backToDashboard = function() {
+5
View File
@@ -112,6 +112,7 @@
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
cursor: pointer;
&.opens {
padding-right: 0.75em;
@@ -166,6 +167,10 @@
}
}
}
.smallerOffenders {
font-size: 0.9em;
}
}
.offendersHtml {
+33 -4
View File
@@ -100,6 +100,11 @@
<div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
</div>
<div ng-if="policyName === 'cssMobileFirst'">
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssParsingErrors'">
<b>{{offender.error}}</b>
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
@@ -143,10 +148,18 @@
</div>
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
{{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
<br>
<b>{{offender.message}}</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
<b>{{offender.property}} {{offender.message}}</b>
<div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
<span ng-if="!offender.showMore">show</span>
<span ng-if="offender.showMore">hide</span>
<ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
</div>
<div ng-if="offender.showMore" class="smallerOffenders">
<div ng-repeat="cssRule in offender.rules">
{{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
<file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
</div>
</div>
</div>
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
@@ -340,6 +353,22 @@
Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
</div>
<div ng-if="policyName === 'cssBreakpoints'">
<div ng-if="rule.value > 0" class="cssBreakpointsGraph">
<h3>Breakpoints distribution graph</h3>
<canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-legend="true" chart-colours="breakpointsColours" width="600" height="250"></canvas>
<h3>Breakpoints list</h3>
<div class="offendersTable">
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
<div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
</div>
</div>
</div>
<div ng-if="rule.value === 0">
No breakpoint
</div>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
+78 -24
View File
@@ -464,7 +464,7 @@ var policies = {
"cssColors": {
"tool": "phantomas",
"label": "Colors count",
"message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS project will be easier to maintain if you keep a small color set.</p>",
"message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS will be easier to maintain if you keep a small color set.</p>",
"isOkThreshold": 30,
"isBadThreshold": 150,
"isAbnormalThreshold": 400,
@@ -522,6 +522,43 @@ var policies = {
};
}
},
"cssBreakpoints": {
"tool": "mediaQueriesChecker",
"label": "Breakpoints count",
"message": "<p>This is the number of different breakpoints found in the stylesheets' media queries.</p><p>Please note this rule is based on <i>min-width</i>, <i>max-width</i>, <i>min-device-width</i> and <i>max-device-width</i> media queries only.</p><p>Your CSS will be easier to maintain if you keep a reasonable number of breakpoints. Try to make a fluid design - using percents - to avoid the creation of numerous breakpoints.</p>",
"isOkThreshold": 6,
"isBadThreshold": 40,
"isAbnormalThreshold": 60,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var offendersTable = [];
for (var offender in offenders) {
offendersTable.push({
breakpoint: offender,
count: offenders[offender].count,
pixels: offenders[offender].pixels
});
}
return offendersTable;
}
},
"cssMobileFirst": {
"tool": "mediaQueriesChecker",
"label": "Not mobile-first media queries",
"message": "<p>This is the number of CSS rules inside media queries that address small screens.</p><p>The common good practice, when creating a responsive website, is to write it \"mobile-first\". More explanation in <a href=\"http://www.sitepoint.com/introduction-mobile-first-media-queries\" target=\"_blank\">this great article</a>.</p>",
"isOkThreshold": 25,
"isBadThreshold": 200,
"isAbnormalThreshold": 1000,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders
};
}
},
"cssImports": {
"tool": "phantomas",
"label": "Uses of @import",
@@ -752,36 +789,53 @@ var policies = {
"cssOldPropertyPrefixes": {
"tool": "phantomas",
"label": "Old prefixes",
"message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>",
"message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. Sometimes, they have never even existed. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p><p>The prefixes database comes from <a href=\"http://caniuse.com/\" target=\"_blank\">Can I Use</a>.</p>",
"isOkThreshold": 0,
"isBadThreshold": 75,
"isAbnormalThreshold": 300,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var properties = {};
offenders.forEach(function(offender) {
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.css);
if (!parts) {
debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
var propertyName = parts[2];
if (!properties[propertyName]) {
properties[propertyName] = {
property: propertyName,
message: parts[4],
rules: []
};
}
properties[propertyName].rules.push({
rule: parts[1],
value: parts[3],
file: splittedOffender.file,
line: splittedOffender.line,
column: splittedOffender.column
});
});
// Object to array
var list = [];
for (var propertyName in properties) {
list.push(properties[propertyName]);
}
return {
count: offenders.length,
list: offenders.map(function(offender) {
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.css);
if (!parts) {
debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
rule: parts[1],
property: parts[2],
value: parts[3],
message: parts[4],
file: splittedOffender.file,
line: splittedOffender.line,
column: splittedOffender.column
};
})
list: list
};
}
},
+3 -1
View File
@@ -71,7 +71,9 @@
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5,
"similarColors": 0.5
"similarColors": 0.5,
"cssBreakpoints": 1,
"cssMobileFirst": 1
}
},
"badCSS": {
+4
View File
@@ -4,6 +4,7 @@ var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var colorDiff = require('./tools/colorDiff');
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
var weightChecker = require('./tools/weightChecker/weightChecker');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -32,6 +33,9 @@ var Runner = function(params) {
// Compare colors
data = colorDiff.compareAllColors(data);
// Check media queries
data = mediaQueriesChecker.analyzeMediaQueries(data);
// Redownload every file
return weightChecker.recheckAllFiles(data);
+4 -2
View File
@@ -46,7 +46,9 @@ var colorDiff = function() {
var deduplicatedColors = {};
parsedOffenders.forEach(function(color) {
deduplicatedColors[color] = color;
if (color !== null) {
deduplicatedColors[color] = color;
}
});
return Object.keys(deduplicatedColors).map(this.parseColor);
@@ -54,7 +56,7 @@ var colorDiff = function() {
this.parseOffender = function(offender) {
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
return regexResult[1];
return regexResult ? regexResult[1] : null;
};
this.parseColor = function(color) {
+20 -16
View File
@@ -121,24 +121,28 @@ var jsExecutionTransformer = function() {
debug('JS execution transformation complete');
debug('Starting scroll execution transformation');
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (offenders.DOMaccessesOnScroll.children) {
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
// Mark a event flag
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
debug('Starting scroll execution transformation');
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (offenders.DOMaccessesOnScroll.children) {
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
// Mark a event flag
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
});
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
});
}
debug('Scroll execution transformation complete');
} else {
debug('Could not parse scrollExecutionTree');
}
debug('Scroll execution transformation complete');
} catch(err) {
throw err;
+168
View File
@@ -0,0 +1,168 @@
var debug = require('debug')('ylt:mediaQueriesChecker');
var parseMediaQuery = require('css-mq-parser');
var offendersHelpers = require('../offendersHelpers');
var mediaQueriesChecker = function() {
'use strict';
var MOBILE_MIN_BREAKPOINT = 200;
var MOBILE_MAX_BREAKPOINT = 300;
this.analyzeMediaQueries = function(data) {
debug('Starting to check all media queries...');
var offenders = data.toolsResults.phantomas.offenders.cssMediaQueries;
var mediaQueries = (offenders) ? this.parseAllMediaQueries(offenders) : [];
var notMobileFirstCount = 0;
var notMobileFirstOffenders = [];
var breakpointsOffenders = {};
for (var i = 0; i < mediaQueries.length; i++) {
var item = mediaQueries[i];
if (!item) {
continue;
}
if (item.isForMobile) {
notMobileFirstCount += item.mediaQuery.rules;
notMobileFirstOffenders.push(item.mediaQuery);
}
for (var j = 0; j < item.breakpoints.length; j++) {
var breakpointString = item.breakpoints[j].string;
if (!breakpointsOffenders[breakpointString]) {
breakpointsOffenders[breakpointString] = {
count: 1,
pixels: item.breakpoints[j].pixels
};
} else {
breakpointsOffenders[breakpointString].count += 1;
}
}
}
data.toolsResults.mediaQueriesChecker = {
metrics: {
cssMobileFirst: notMobileFirstCount,
cssBreakpoints: Object.keys(breakpointsOffenders).length
},
offenders: {
cssMobileFirst: notMobileFirstOffenders,
cssBreakpoints: breakpointsOffenders
}
};
debug('End of media queries check');
return data;
};
this.parseAllMediaQueries = function(offenders) {
return offenders.map(this.parseOneMediaQuery);
};
this.parseOneMediaQuery = function(offender) {
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
var parts = /^@media (.*) \((\d+ rules)\)$/.exec(splittedOffender.css);
if (!parts) {
debug('Failed to parse media query ' + offender);
return false;
}
var rulesCount = parseInt(parts[2], 10);
var query = parts[1];
var isForMobile = false;
var breakpoints = [];
try {
var ast = parseMediaQuery(query);
var min = 0;
var max = Infinity;
var pixels;
ast.forEach(function(astItem) {
astItem.expressions.forEach(function(expression) {
if (expression.feature === 'width' || expression.feature === 'device-width') {
if (astItem.inverse === false) {
if (expression.modifier === 'max') {
pixels = toPixels(expression.value);
max = Math.min(max, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
} else if (expression.modifier === 'min') {
pixels = toPixels(expression.value);
min = Math.max(min, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
}
} else if (astItem.inverse === true) {
if (expression.modifier === 'max') {
pixels = toPixels(expression.value);
min = Math.max(min, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
} else if (expression.modifier === 'min') {
pixels = toPixels(expression.value);
max = Math.min(max, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
}
}
}
});
});
isForMobile = (min <= MOBILE_MIN_BREAKPOINT && max >= MOBILE_MAX_BREAKPOINT && max !== Infinity);
} catch(error) {
debug('Failed to parse media query ' + offender);
}
return {
mediaQuery: {
query: query,
rules: rulesCount,
file: splittedOffender.file,
line: splittedOffender.line,
column: splittedOffender.column
},
isForMobile: isForMobile,
breakpoints: breakpoints
};
};
// Parses a size in em, pt (or px) and returns it in px
function toPixels(size) {
var splittedSize = /^([\d\.]+)(.*)/.exec(size);
var value = parseFloat(splittedSize[1]);
var unit = splittedSize[2];
if (unit === 'em') {
return value * 16;
}
if (unit === 'pt') {
return value / 12 * 16;
}
return value;
}
};
module.exports = new mediaQueriesChecker();
@@ -0,0 +1,79 @@
/**
* Analyzes images and detects which one can be lazy-loaded (are below the fold)
*
* @see https://github.com/macbre/phantomas/issues/494
*/
/* global document: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded
phantomas.on('report', function() {
phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...');
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded');
var images = document.body.getElementsByTagName('img'),
i,
len = images.length,
offset,
path,
processedImages = {},
src,
viewportHeight = window.innerHeight;
phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
for (i = 0; i < len; i++) {
// @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
offset = images[i].getBoundingClientRect().top;
src = images[i].src;
// ignore base64-encoded images
if (src === '' || /^data:/.test(src)) {
continue;
}
path = phantomas.getDOMPath(images[i]);
// get the most top position for a given image (deduplicate by src)
if (typeof processedImages[src] === 'undefined') {
processedImages[src] = {
offset: offset,
path: path
};
}
// maybe there's the same image loaded above the fold?
if (offset < processedImages[src].offset) {
processedImages[src] = {
offset: offset,
path: path
};
}
}
phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
Object.keys(processedImages).forEach(function(src) {
var img = processedImages[src];
if (img.offset > viewportHeight) {
phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
}
});
phantomas.spyEnabled(true);
})(window.__phantomas);
});
});
};
+21 -10
View File
@@ -23,7 +23,7 @@ var PhantomasWrapper = function() {
'engine': task.options.phantomasEngine || 'webkit',
'timeout': task.options.timeout || 30,
'js-deep-analysis': task.options.jsDeepAnalysis || false,
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36' : null,
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/27.0.1453.110 Safari/537.36' : null,
'tablet': (task.options.device === 'tablet'),
'phone': (task.options.device === 'phone'),
'screenshot': task.options.screenshot || false,
@@ -46,6 +46,7 @@ var PhantomasWrapper = function() {
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
'jQuery', // overridden
'jserrors', // overridden
'lazyLoadableImages', //overriden
'pageSource', // not needed
'windowPerformance' // overriden
].join(','),
@@ -112,9 +113,19 @@ var PhantomasWrapper = function() {
// It's time to launch the test!!!
var triesNumber = 2;
var currentTry = 0;
async.retry(triesNumber, function(cb) {
currentTry ++;
// Fix for https://github.com/gmetais/YellowLabTools/issues/114
if (currentTry === 2 && options.engine === 'webkit2') {
debug('Launching a second try with the old webkit v1 engine');
options.engine = 'webkit';
}
var process = phantomas(task.url, options, function(err, json, results) {
var errorCode = err ? parseInt(err.message, 10) : null;
if (isKilled) {
debug('Process was killed, too late Phantomas, sorry...');
@@ -122,28 +133,28 @@ var PhantomasWrapper = function() {
}
debug('Returning from Phantomas with error %s', err);
debug('Returning from Phantomas with error %s', errorCode);
// Adding some YellowLabTools errors here
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
err = 1001;
errorCode = 1001;
}
if (!err && (!json || !json.metrics)) {
err = 1002;
if (!errorCode && (!json || !json.metrics)) {
errorCode = 1002;
}
// Don't cancel test if it is a timeout and we've got some results
if (err === 252 && json) {
if (errorCode === 252 && json) {
debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
err = null;
errorCode = null;
}
if (err) {
debug('Attempt failed. Error code ' + err);
if (errorCode) {
debug('Attempt failed. Error code ' + errorCode);
}
cb(err, json);
cb(errorCode, json);
});
phantomasPid = process.pid;
+36 -31
View File
@@ -1,6 +1,6 @@
{
"name": "yellowlabtools",
"version": "1.8.0",
"version": "1.9.1",
"description": "Online tool to audit a webpage for performance and front-end quality issues",
"license": "GPL-2.0",
"author": {
@@ -15,51 +15,56 @@
"bin": {
"yellowlabtools": "./bin/cli.js"
},
"engines": {
"node": ">= 0.12.0"
},
"main": "./lib/index.js",
"dependencies": {
"angular": "1.4.5",
"angular-animate": "1.4.5",
"angular-chart.js": "0.8.4",
"angular": "1.4.8",
"angular-animate": "1.4.8",
"angular-chart.js": "0.8.6",
"angular-local-storage": "0.2.2",
"angular-resource": "1.4.5",
"angular-route": "1.4.5",
"angular-sanitize": "1.4.5",
"async": "1.4.2",
"body-parser": "1.14.0",
"angular-resource": "1.4.8",
"angular-route": "1.4.8",
"angular-sanitize": "1.4.8",
"async": "1.5.0",
"body-parser": "1.14.1",
"chart.js": "1.0.2",
"clean-css": "3.4.3",
"clean-css": "3.4.8",
"color-diff": "0.1.7",
"compression": "1.5.2",
"compression": "1.6.0",
"cors": "2.7.1",
"css-mq-parser": "0.0.3",
"debug": "2.2.0",
"express": "4.13.3",
"imagemin": "3.2.0",
"imagemin-jpegoptim": "4.0.0",
"imagemin": "4.0.0",
"imagemin-jpegoptim": "4.1.0",
"jstoxml": "0.2.3",
"lwip": "0.0.7",
"meow": "3.3.0",
"minimize": "1.7.1",
"lwip": "0.0.8",
"meow": "3.6.0",
"minimize": "1.7.4",
"parse-color": "1.0.0",
"phantomas": "1.12.0",
"ps-node": "0.0.4",
"phantomas": "1.13.0",
"ps-node": "0.0.5",
"q": "1.4.1",
"request": "2.62.0",
"rimraf": "2.4.3",
"request": "2.67.0",
"rimraf": "2.4.4",
"temporary": "0.0.8",
"uglify-js": "2.4.24"
"try-thread-sleep": "1.0.0",
"uglify-js": "2.6.1"
},
"devDependencies": {
"chai": "~3.2.0",
"chai": "~3.4.0",
"grunt": "~0.4.5",
"grunt-blanket": "~0.0.8",
"grunt-contrib-clean": "~0.6.0",
"grunt-blanket": "~0.0.10",
"grunt-contrib-clean": "~0.7.0",
"grunt-contrib-concat": "~0.5.1",
"grunt-contrib-copy": "~0.8.1",
"grunt-contrib-copy": "~0.8.2",
"grunt-contrib-cssmin": "~0.14.0",
"grunt-contrib-htmlmin": "~0.4.0",
"grunt-contrib-htmlmin": "~0.6.0",
"grunt-contrib-jshint": "~0.11.3",
"grunt-contrib-less": "~1.0.1",
"grunt-contrib-uglify": "~0.9.2",
"grunt-contrib-less": "~1.1.0",
"grunt-contrib-uglify": "~0.11.0",
"grunt-env": "~0.4.4",
"grunt-express": "~1.4.1",
"grunt-filerev": "~2.3.1",
@@ -68,10 +73,10 @@
"grunt-mocha-test": "~0.12.7",
"grunt-replace": "~0.11.0",
"grunt-usemin": "~3.1.1",
"grunt-webfont": "~0.5.4",
"matchdep": "~0.3.0",
"grunt-webfont": "~1.0.2",
"matchdep": "~1.0.0",
"mocha": "~2.3.2",
"sinon": "~1.16.1",
"sinon": "~1.17.2",
"sinon-chai": "~2.8.0"
},
"scripts": {
+55
View File
@@ -0,0 +1,55 @@
var should = require('chai').should();
var mediaQueriesChecker = require('../../lib/tools/mediaQueriesChecker');
describe('mediaQueriesChecker', function() {
it('should parse mediaQueryes correctly', function() {
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <http://domain.com/css/stylesheet.css> @ 269:1').should.deep.equal({
mediaQuery: {
query: 'screen and (max-width: 1024px)',
rules: 1,
file: 'http://domain.com/css/stylesheet.css',
line: 269,
column: 1
},
isForMobile: true,
breakpoints: [{string: '1024px', pixels: 1024}]
});
});
it('should determine if it\'s for mobile correctly', function() {
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 290px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 320px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 20em) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 40em) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 400px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 290px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 400px) and (max-width: 180px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
});
it('should count breakpoints correctly', function() {
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '320px', pixels: 320}]);
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '600px', pixels: 600}, {string: '1000px', pixels: 1000}]);
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '180px', pixels: 180}]);
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1000px', pixels: 1000}, {string: '600px', pixels: 600}]);
mediaQueriesChecker.parseOneMediaQuery('@media (max-height:500px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([]);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 100em) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '100em', pixels: 1600}]);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 120pt) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '120pt', pixels: 160}]);
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 40.2em) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '40.2em', pixels: 643.2}]);
});
it('should fail silently', function() {
mediaQueriesChecker.parseOneMediaQuery('@media bad syntax (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
});
});
+20 -13
View File
@@ -51,11 +51,14 @@ describe('phantomasWrapper', function() {
done('Error: unwanted success');
}).fail(function(err) {
try {
should.exist(err);
err.should.equal(254);
should.exist(err);
err.should.equal(254);
done();
done();
} catch(error) {
done(error);
}
});
});
@@ -72,16 +75,20 @@ describe('phantomasWrapper', function() {
}
}).then(function(data) {
/*jshint -W030 */
data.should.be.an('object');
data.should.have.a.property('generator');
data.generator.should.contain('phantomas');
data.should.have.a.property('url').that.equals(url);
data.should.have.a.property('metrics').that.is.an('object').not.empty;
data.should.have.a.property('offenders').that.is.an('object').not.empty;
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
done();
try {
data.should.be.an('object');
data.should.have.a.property('generator');
data.generator.should.contain('phantomas');
data.should.have.a.property('url').that.equals(url);
data.should.have.a.property('metrics').that.is.an('object').not.empty;
data.should.have.a.property('offenders').that.is.an('object').not.empty;
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
done();
} catch(error) {
done(error);
}
}).fail(function(err) {
done(err);
});