Compare commits

...
124 Commits
Author SHA1 Message Date
Gaël Métais f423c5dc4e Start working on CDN detection 2021-03-07 09:41:22 +00:00
Gaël Métais 0a1b9ce27a Fix JS error offenders 2021-03-02 00:19:56 +00:00
Gaël Métais bb9429e907 Update Imagemin and dependencies 2021-03-01 02:50:23 +00:00
Gaël Métais 1213854644 Update Imagemin and dependencies 2021-03-01 01:31:35 +00:00
Gaël Métais e3d4100055 Update ttf2woff2 library 2021-03-01 01:18:48 +00:00
Gaël Métais 418426419d Improve compression detection 2021-02-28 13:59:16 +00:00
Gaël Métais 707048a1df Change Brotli wording 2021-02-26 16:22:29 +00:00
Gaël Métais e9ab1fd16e Sorry, I should have kept is-json package 2021-02-25 15:21:45 +00:00
Gaël Métais cc74359e83 Fix SSL errors not being correctly ignored 2021-02-20 13:02:14 +00:00
Gaël Métais 9be862e37b Remove is-json package 2021-02-19 20:08:13 +00:00
Gaël Métais 2133c1e16a Merge branch 'develop' of https://github.com/YellowLabTools/YellowLabTools into develop 2021-02-19 20:04:08 +00:00
Gaël Métais e46d4cff27 Fix offenders not being displayed for globalVariables and synchronousXHR 2021-02-19 20:03:02 +00:00
Gaël Métais 38b362e5fe Comment typo 2021-02-15 22:46:42 +00:00
Gaël Métais 6d01461e53 Improve JSON detection when the mime-type is not application/json 2021-02-14 18:32:05 +00:00
Gaël MétaisandGitHub d40d4ec7fb Merge branch 'master' into develop 2021-02-03 23:21:30 +00:00
Gaël Métais 96f720c195 Update latest jQuery version 2021-02-03 21:35:33 +00:00
Gaël Métais 3f5c54d5de Remove the Tweet this button 2021-02-02 00:24:26 +00:00
Gaël Métais c62805ae57 Slightly increase DOM max depth threshold 2021-02-01 23:44:52 +00:00
Gaël Métais acb1b72036 Remove the CSS Expressions metric, too rare 2021-02-01 16:22:17 +00:00
Gaël Métais 443f452689 Fix a bug with the favicon spinner 2021-02-01 15:10:30 +00:00
Gaël Métais 72a347aa9f Fix bug with EJS on production env 2021-01-31 01:42:03 +00:00
Gaël Métais ab7ffa0d94 Replace grunt-replace with EJS 2021-01-31 00:29:37 +00:00
Gaël Métais 1259439603 Fix device icons on Safari 2021-01-31 00:09:52 +00:00
Gaël Métais 208ed63c96 Fix TTF files not being converted to WOFF2 2021-01-30 23:56:03 +00:00
Gaël Métais dd439d5ec3 Don't try to convert huge fonts to woff2, otherwise the server freezes for too long 2021-01-30 23:55:28 +00:00
Gaël Métais 44a14f326b Add tmp/ to .gitignore 2021-01-30 23:05:27 +00:00
Gaël Métais 4d4a9523f8 Wording 2021-01-25 12:24:38 +00:00
Gaël Métais b24a676f22 Change about page's display on mobile 2021-01-24 14:34:06 +00:00
Gaël Métais 4e692d608c Various wordings changed on home and about pages 2021-01-24 14:10:02 +00:00
Gaël Métais b54eb0cca9 New API entry that counts the number of pending tests 2021-01-24 00:30:28 +00:00
Gaël Métais 24ad2f45cc Replace the recommended lazyloading library 2021-01-24 00:29:45 +00:00
Gaël Métais 27b5ce8d60 Removing the ability to scroll on the screenshot as they will only show above the fold 2021-01-22 15:57:03 +00:00
Gaël Métais 1ce350b12e Chromium requires more package dependencies than before 2021-01-22 15:00:15 +00:00
Gaël Métais 405d53220b Fix server stability issue 2021-01-14 00:24:22 +00:00
Gaël Métais 3123e90d5a Make the Similar colors detector more relax 2021-01-12 09:57:50 +00:00
Gaël Métais ea7a9811e4 Improve WP detection 2021-01-10 23:58:05 +00:00
Gaël Métais 74b04ff922 Improve WP detection 2021-01-10 23:50:44 +00:00
Gaël Métais 4b24cd68ce Debugging 2021-01-10 23:39:54 +00:00
Gaël Métais c94089842a Improve wording 2021-01-09 22:12:13 +00:00
Gaël Métais 9c0743d668 Fix UI on the Brotli rule 2021-01-09 22:05:27 +00:00
Gaël Métais d023af20a0 Add a Buy me a coffee button 2021-01-08 02:52:47 +00:00
Gaël Métais b81ddd8dd9 Add a Buy me a coffee button 2021-01-08 02:51:59 +00:00
Gaël Métais c1e7f58567 Add a Buy me a coffee button 2021-01-08 02:50:58 +00:00
Gaël Métais 8c055d0b72 Adding a bunch of debian packages required by Chrome Headless 2021-01-08 02:44:53 +00:00
Gaël Métais 44a30f7a0d Sponsorship 2021-01-08 02:43:10 +00:00
Gaël Métais f837ad2270 Probably fixes a bug that stucks the server 2021-01-08 02:42:01 +00:00
Gaël Métais 6b909bd0e3 Clean up error 500 page 2021-01-07 21:31:37 +00:00
Gaël Métais 23d7dea0bb Improve WordPress detection 2021-01-07 21:14:35 +00:00
Gaël MétaisandGitHub fc13053545 Merge pull request #322 from YellowLabTools/ylt-v2
YellowLab Tools V2 - beta version
2021-01-06 23:28:49 +00:00
Gaël MétaisandGitHub e874ccb5b1 Merge branch 'develop' into ylt-v2 2021-01-06 23:14:34 +00:00
Gaël Métais fd11e8c1fe In some rare cases, the HTML minifier can be very slow, timeouting other simultaneous requests 2021-01-05 22:19:02 +00:00
Gaël Métais 294c5ab06d Adding favicon animation and color codes 2021-01-03 21:44:47 +00:00
Gaël Métais 576dac3aff Adding favicon animation and color codes 2021-01-03 19:06:55 +00:00
Gaël Métais 4b80310c0d Fix settings files 2021-01-03 01:49:11 +00:00
Gaël Métais bd9cb2e257 Add a progress bar on the UI 2021-01-03 01:43:46 +00:00
Gaël Métais 64733d55f8 Wording 2021-01-02 22:24:42 +00:00
Gaël Métais 88483af607 Detect WordPress for optional sponsoring message 2021-01-02 17:14:51 +00:00
Gaël Métais 1726e023ea Rewording 2021-01-02 15:57:00 +00:00
Gaël Métais 7e9b12f818 Adjust scoring 2020-12-30 00:52:18 +01:00
Gaël Métais 834a9c9ac5 Remove the Small Requests rule 2020-12-30 00:39:56 +01:00
Gaël Métais 3f098cd33a Give less score weight to requests number 2020-12-30 00:37:33 +01:00
Gaël Métais 4193d61e0b Remove uselss code 2020-12-30 00:26:36 +01:00
Gaël Métais e8d464a244 If you fix this rule, your global score increases by X points 2020-12-30 00:21:29 +01:00
Gaël Métais 4c1c375eef Fix tests starting before the end of the previous one 2020-12-29 22:45:29 +01:00
Gaël Métais 65a2bbae0a Adjust scores to nowadays browsers and network 2020-12-29 11:56:54 +01:00
Gaël Métais 57616aba62 Fix wording 2020-12-29 11:55:57 +01:00
Gaël Métais 4396bd4625 White background not needed & optimize images more 2020-12-29 11:55:22 +01:00
Gaël Métais df4f5685be New metric Total JS execution time 2020-12-23 15:24:48 +01:00
Gaël Métais 1e5f797195 Fix http2 offenders page 2020-12-22 12:27:35 +01:00
Gaël Métais 8f06ceaedd Compare minification with brotli instead of gzip 2020-12-22 00:00:44 +01:00
Gaël Métais 6b58b72028 Replace brotli module by zlib 2020-12-21 23:59:39 +01:00
Gaël Métais 51dc1b1618 Add Brotli compression as the default compresion 2020-12-21 10:21:20 +01:00
Gaël Métais be5549bf81 Replace the HTML minifier so that it minifies inline JS and CSS 2020-12-21 02:14:30 +01:00
Gaël Métais c4b7bd4d71 Fix error 2020-12-21 02:09:24 +01:00
Gaël Métais f25eb2901e Remove old phantomas modules 2020-12-21 01:32:43 +01:00
Gaël Métais 65025b57c5 Fix syntax error 2020-12-21 01:28:01 +01:00
Gaël Métais 8e3b212df0 Linting 2020-12-21 01:27:34 +01:00
Gaël Métais d93af3d6e4 Detect non WOFF2 fonts and estimate gain 2020-12-21 01:18:56 +01:00
Gaël Métais dd0c17a9d4 Fix content type checking after switching to phantomas v2 2020-12-19 23:44:36 +01:00
Gaël Métais d334726991 Fix offendersdisplay on jquery versions page 2020-11-26 19:58:46 +01:00
Gaël Métais 9aea4e5677 New metrics: old HTTP and TLS protocols 2020-11-26 01:37:58 +01:00
Gaël Métais ffd9efeb98 New DOMaccess agregated form + new score category JS complexity 2020-11-24 00:18:55 +01:00
Gaël Métais 5bf3bd7ce5 Fix a potential divided by O error 2020-11-23 13:49:04 +01:00
Gaël Métais 693fce1bb1 Flag as alpha version 2020-11-10 15:00:02 +01:00
Gaël Métais 48fbb1076f Adjust design 2020-10-06 08:30:48 +02:00
Gaël Métais 7bc994b99f Documentation 2020-10-06 00:59:14 +02:00
Gaël Métais 8586d1cc12 Documentation 2020-10-06 00:58:36 +02:00
Gaël Métais de280c23ea Increase timeout to 2 minutes 2020-10-06 00:55:29 +02:00
Gaël Métais 06887003cf lib/tools/phantomas/phantomasWrapper.js 2020-10-06 00:54:54 +02:00
Gaël Métais e2eacbc051 Responsive design for mobile (finally) 2020-10-05 12:49:26 +02:00
Gaël Métais 93b81493c0 Graphical changes, essentially colors 2020-10-04 18:50:28 +02:00
Gaël Métais 4287f60ab3 Adapt Oversized Images detection to our new screen resolutions 2020-10-04 13:23:43 +02:00
Gaël Métais f62e9baa4a Add a new Desktop HP device (large DPR2 screen) 2020-10-04 10:09:06 +02:00
Gaël Métais 2d2da6e939 Fix icons 2020-10-04 02:57:53 +02:00
Gaël Métais 2bf3d5372b Fix screenshots and continue implementing phantomas v2 2020-10-04 02:54:00 +02:00
Gaël Métais a4f8e20293 Fix screenshots and continue implementing phantomas v2 2020-10-04 02:44:16 +02:00
Gaël Métais ac915e8973 Fix screenshots and continue implementing phantomas v2 2020-10-04 02:43:10 +02:00
Gaël Métais e19a521c4e Various adaptations for Phantomas v2 2020-08-11 03:01:43 +02:00
Gaël Métais bafea756ee Add brotli decompression to redownload.js 2020-08-11 02:04:59 +02:00
Gaël Métais 1bdfc8a5d2 Rename blacklist and whitelist into block list and allowlist 2020-08-11 01:37:32 +02:00
Gaël Métais 6e6285c5ae Various adaptations for Phantomas v2 2020-08-10 13:56:37 +02:00
Gaël Métais f49411b3a9 Various adaptations for Phantomas v2 2020-08-10 01:03:42 +02:00
Gaël Métais 1d3dee1950 Fix image optimization 2020-08-09 02:06:20 +02:00
Gaël Métais caf3ddd68a Font related stuff not needed anymore 2020-08-09 01:52:23 +02:00
Gaël Métais 17b5168bde Node compatibility set to >=12.0 2020-08-09 01:28:25 +02:00
Gaël Métais 9bfc16a4d5 Get rid of the icons font 2020-08-09 01:13:09 +02:00
Gaël Métais 98f5245317 Create an error page in case YLT does not respond (allong with nginx instructions) 2020-08-08 09:55:07 +02:00
Gaël Métais 4286c528f5 Fix GitHub button size 2020-08-08 09:52:51 +02:00
Gaël MétaisandGitHub 8f5f9d6238 1.13.4 (#305)
* Update GitHub repository URL
* Update jQuery version
2020-02-04 12:40:57 -03:00
Gaël Métais 26d3a53d44 Merge branch 'master' into develop 2020-02-04 12:37:04 -03:00
Gaël Métais de8e444aba Update jQuery version 2020-02-04 12:36:45 -03:00
Gaël Métais 35da556697 Update GitHub repository URL 2020-02-04 12:36:10 -03:00
PurpleBabarandGaël Métais 52ac5e5424 Dockerize app (#303)
Add a Dockerfile to the project
2020-01-17 11:10:53 -03:00
Gaël Métais 517206364a Work in progress: switch to Phantomas v2 2019-03-14 03:27:37 +01:00
Gaël Métais 5e82eea9f1 Remove JS Timeline and Scroll Bottlenecks 2019-03-13 19:27:57 +01:00
Gaël MétaisandGitHub b6e8768767 Update dependencies (#280)
* Remove an always failing test
* Update dependencies
* Revert 'temporary' npm package to v0.0.8
* Update Node version in Vagrantfile
2019-03-13 15:51:12 +00:00
Gaël Métais 5df3d4f63b Add package-lock.json to .gitignore 2019-02-18 12:57:19 +01:00
Gaël Métais c59e9fc3f5 Merge branch 'develop' 2019-01-13 17:39:27 +05:30
Gaël Métais febe9871b9 Fix missing text 2019-01-13 17:39:11 +05:30
Gaël Métais 4d5d881af6 Merge branch 'develop' 2019-01-13 17:21:48 +05:30
Gaël Métais 2735deff73 v1.13.3 2019-01-13 17:21:29 +05:30
Gaël Métais ee93add6fc Remove failing 4.7 tests on Travis 2019-01-13 17:19:54 +05:30
Gaël MétaisandGitHub 8ffab2ae70 New rule: oversized images (#276)
New rule: oversized images
2019-01-13 17:18:03 +05:30
Gaël Métais fa216dddd5 Fix typo 2019-01-07 12:59:57 +05:30
120 changed files with 29966 additions and 5840 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+3 -1
View File
@@ -1,8 +1,10 @@
node_modules
package-lock.json
.tmp
tmp
.vagrant
results/*
coverage
front/build
package-lock.json
har.json
+2 -3
View File
@@ -1,9 +1,8 @@
language: node_js
sudo: false
node_js:
- "8.9"
- "6.2"
- "4.7"
- "12.18"
- "14.7"
env:
- CXX=g++-4.8
addons:
+7
View File
@@ -0,0 +1,7 @@
FROM node:10
WORKDIR /app
ENV VERSION=master
EXPOSE 8383
RUN git clone --branch ${VERSION} https://github.com/LumberjackOtters/YellowLabTools ylt && cd ylt && yarn install && yarn build
ENV NODE_ENV=production
CMD ["node", "/app/ylt/bin/server.js"]
+1
View File
@@ -0,0 +1 @@
ko_fi: gaelmetais
+15 -44
View File
@@ -11,21 +11,6 @@ module.exports = function(grunt) {
pkg: grunt.file.readJSON('package.json'),
settings: grunt.file.readJSON('./server_config/settings.json'),
webfont: {
icons: {
src: 'front/src/fonts/svg-icons/*.svg',
dest: 'tmp',
destCss: 'front/src/less',
options: {
engine: 'node',
types: 'woff',
stylesheet: 'less',
embed: true,
htmlDemo: false,
syntax: 'bootstrap'
}
}
},
less: {
all: {
files: [
@@ -39,25 +24,6 @@ module.exports = function(grunt) {
]
}
},
replace: {
dist: {
options: {
patterns: [
{
match: 'googleAnalyticsId',
replacement: '<%= settings.googleAnalyticsId %>'
},
{
match: 'version',
replacement: 'v<%= pkg.version %>'
}
]
},
files: [
{expand: true, flatten: true, src: ['front/src/main.html'], dest: 'front/build/'}
]
}
},
jshint: {
all: [
'*.js',
@@ -71,7 +37,10 @@ module.exports = function(grunt) {
'test/core/*.js',
'test/fixtures/*.js',
'front/src/js/**/*.js'
]
],
options: {
esversion: 6
}
},
clean: {
tmp: {
@@ -87,9 +56,17 @@ module.exports = function(grunt) {
copy: {
build: {
files: [
{src: ['./front/src/main.html'], dest: './front/build/main.html'},
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
{src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
]
},
favicons: {
files: [
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
{src: ['./front/src/img/favicon-fail.png'], dest: './front/build/img/favicon-fail.png'},
{src: ['./front/src/img/favicon-success.png'], dest: './front/build/img/favicon-success.png'},
]
}
},
mochaTest: {
@@ -103,7 +80,7 @@ module.exports = function(grunt) {
options: {
reporter: 'spec',
},
src: ['test/api/apiTest.js']
src: ['test/core/mediaQueriesCheckerTest.js']
}
},
env: {
@@ -222,12 +199,6 @@ module.exports = function(grunt) {
process.env.IS_TEST = true;
});
grunt.registerTask('icons', [
'webfont:icons',
'less',
'clean:tmp'
]);
grunt.registerTask('build', [
'jshint',
'clean:build',
@@ -237,13 +208,13 @@ module.exports = function(grunt) {
'concat',
'uglify',
'cssmin',
'replace',
'htmlmin:views',
'inline_angular_templates',
'filerev',
'usemin',
'htmlmin:main',
'clean:tmp'
'clean:tmp',
'copy:favicons'
]);
grunt.registerTask('hint', [
+10 -8
View File
@@ -14,12 +14,12 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
<a href="https://yellowlab.tools" target="_blank">https://yellowlab.tools</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-animated.gif"></img>
<img src="./doc/img/home.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-cli-animated.gif"></img>
@@ -36,7 +36,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
</tr>
<tr>
<td width="70%">
The <b>NodeJS</b> package - <a href="https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
The <b>NodeJS</b> package - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/npm-logo.png"></img>
@@ -44,7 +44,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
</tr>
<tr>
<td width="70%">
The <b>public API</b> - <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
The <b>public API</b> - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
<br>
Allows you to launch runs from any language
</td>
@@ -58,9 +58,9 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
## How it works
The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
The tool loads the given URL and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
By the way, it's free because I am a geek, not businessmen. In return, you can add a ★ on GitHub, it will boost my motivation to add more awesome features!!!
![example dashboard screenshot](./doc/img/screenshot.png)
@@ -72,18 +72,20 @@ You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel bet
## Install your own private instance
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).
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/YellowLabTools/YellowLabTools/wiki/Install-your-private-server).
## Author
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!
<a href='https://ko-fi.com/gaelmetais' target='_blank'><img height='35' style='border:0px;height:46px;' src='https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0' border='0' alt='Buy me a coffee' /><a>
I can also help your company about Web Performances, visit [my website](https://www.gaelmetais.com).
## Contributors
- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
- [camlafit](https://github.com/camlafit)
- Km [camlafit](https://github.com/camlafit)
- Vincent L. [magikcypress](https://github.com/magikcypress)
- Ousama Ben Younes [ousamabenyounes](https://github.com/ousamabenyounes)
+5 -5
View File
@@ -13,20 +13,20 @@ var cli = meow({
' yellowlabtools <url> <options>',
'',
'Options:',
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
' --device Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.',
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
//' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".',
' --cookie Adds a cookie on the main domain.',
' --auth-user Basic HTTP authentication username.',
' --auth-pass Basic HTTP authentication password.',
' --block-domain Disallow requests to given (comma-separated) domains - aka blacklist.',
' --allow-domain Only allow requests to given (comma-separated) domains - aka whitelist.',
' --block-domain Disallow requests to given (comma-separated) domains.',
' --allow-domain Only allow requests to given (comma-separated) domains.',
' --no-externals Block all domains except the main one.',
' --reporter The output format: "json" or "xml". Default is "json".',
''
].join('\n'),
pkg: '../package.json'
pkg: require('../package.json')
});
@@ -55,7 +55,7 @@ if (screenshot) {
}
// Device simulation
options.device = cli.flags.device || 'desktop';
options.device = cli.flags.device || 'mobile';
// Wait for CSS selector
options.waitForSelector = cli.flags.waitForSelector || null;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 45 KiB

+10 -2
View File
@@ -1,10 +1,18 @@
.about {
margin: 3em auto;
width: 50%;
width: 80%;
}
@media (min-width: 640px) {
.about {
width: 50%;
}
}
.about p {
margin: 2em;
}
.about a {
color: #FFF;
color: #fff;
}
.sponsor {
color: #ffa319;
}
+81 -60
View File
@@ -5,15 +5,8 @@
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;
@@ -21,12 +14,10 @@
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 {
@@ -54,28 +45,63 @@
top: 1.5em;
left: 0.9em;
}
@media (min-width: 820px) {
.summary .globalScore {
width: 65%;
display: table;
}
.summary .globalScore > div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
}
.summary .notations {
width: 100%;
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
margin: 0 0 1.5em;
border-spacing: 0 1em;
}
@media (min-width: 820px) {
.summary .notations {
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;
}
@media (min-width: 820px) {
.summary .notations > div > div {
display: table-cell;
height: 2.5em;
}
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
font-size: 1.2em;
width: 50%;
float: left;
text-align: left;
margin: 0.5em 0.25em;
}
@media (min-width: 820px) {
.summary .notations .category {
width: 20%;
text-align: center;
float: none;
}
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
}
@media (min-width: 820px) {
.summary .notations .criteria {
width: 75%;
}
}
.summary .notations .A.categoryScore,
.summary .notations .B.categoryScore,
@@ -87,10 +113,23 @@
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
margin: 0.2em;
font-size: 1.5em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
float: right;
}
@media (min-width: 820px) {
.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 {
float: none;
font-size: 2em;
}
}
.summary .notations .grade .A,
.summary .notations .grade .B,
@@ -114,14 +153,14 @@
color: inherit;
}
.summary .notations .criteria .table > a:hover > div {
background: #EBD8E2;
background: #d8ebe0;
cursor: pointer;
}
.summary .notations .criteria .table > a:hover > div.info {
background: #FFF;
}
.summary .notations .criteria .table > a:hover > div.info .icon-question {
color: #EBD8E2;
.summary .notations .criteria .table > a:hover > div.info svg {
fill: #d8ebe0;
}
.summary .notations .criteria .grade {
width: 10%;
@@ -134,57 +173,39 @@
}
.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 {
.summary .notations .warning .result {
color: #FF1919;
}
.summary .notations .icon-warning svg {
fill: #FF1919;
margin: -2px 0;
}
.summary .notations .criteria .info {
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
display: none;
}
.summary .notations .criteria .icon-question {
color: transparent;
@media (min-width: 820px) {
.summary .notations .criteria .info {
display: table-cell;
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
}
.summary .fromShare {
margin-bottom: 3em;
}
.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;
.summary .notations .criteria .info svg {
fill: transparent;
}
.summary .sponsor {
font-size: 0.9em;
margin-bottom: 4em;
color: #413;
color: #ffa319;
}
.summary .sponsor a {
color: inherit;
File diff suppressed because one or more lines are too long
+26 -12
View File
@@ -20,7 +20,7 @@
background: #e74c3c;
}
.launchBtn.disabled {
background: #deaca6;
background: #f1bd70;
}
.launchBtn.disabled:focus {
color: #ddd;
@@ -55,23 +55,29 @@
text-decoration: none;
font-size: 0.8em;
}
.device .item > svg {
display: block;
margin: 0.6em auto 0.3em;
fill: #fff;
}
.device .item.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
.device .item.active > svg {
fill: #ffa319;
}
.device .item:hover {
color: #ffa319;
}
.device .item > div {
margin: 0.2em 0 0.1em;
font-size: 3em;
.device .item:hover > svg {
fill: #ffa319;
}
.settingsTooltip {
position: relative;
}
.settingsTooltip span {
font-size: 0.8em;
.settingsTooltip svg {
vertical-align: text-top;
}
.settingsTooltip div {
@@ -85,6 +91,8 @@
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
word-break: break-all;
word-break: break-word;
z-index: 2;
}
.settingsTooltip:hover div {
@@ -149,12 +157,19 @@
width: 50%;
margin: 6em auto 0;
font-size: 0.9em;
color: #413;
color: #8abfaf;
}
.features > div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
@media (min-width: 640px) {
.features > div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
}
.features h3 {
font-size: 1.5em;
font-weight: normal;
color: #fff;
}
input[type=submit],
input.url {
@@ -164,7 +179,6 @@ input.url {
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
+70 -94
View File
File diff suppressed because one or more lines are too long
+13
View File
@@ -6,3 +6,16 @@
font-size: 0.8em;
margin-bottom: 6em;
}
.progressBarEmpty {
width: 90%;
max-width: 300px;
margin: 1em auto;
padding: 0.05em;
border: 1px solid #ffa319;
}
.progressBarFilled {
width: 5%;
height: 0.5em;
background: #ffa319;
transition: width 3s ease-out;
}
+26 -17
View File
@@ -2,7 +2,6 @@
text-align: center;
}
.rule .ruleTable {
display: table;
border-spacing: 1em;
width: 90%;
margin: 2em auto;
@@ -10,16 +9,20 @@
border: 1px dashed #666;
border-radius: 0.5em;
}
.rule .ruleTable > div {
display: table-cell;
vertical-align: middle;
}
.rule .ruleTable .left {
width: 33%;
font-weight: bold;
}
.rule .ruleTable .right {
width: 67%;
@media (min-width: 820px) {
.rule .ruleTable {
display: table;
}
.rule .ruleTable > div {
display: table-cell;
vertical-align: middle;
}
.rule .ruleTable .left {
width: 33%;
}
.rule .ruleTable .right {
width: 67%;
}
}
.rule .score {
font-size: 2.5em;
@@ -27,7 +30,7 @@
height: 2em;
width: 2em;
border-radius: 0.5em;
margin: 0 auto 0.5em;
margin: 0 auto 0.25em;
}
.rule h3 {
margin-bottom: 0em;
@@ -67,7 +70,13 @@
border-spacing: 0 0.25em;
margin: 0 auto;
min-width: 10%;
max-width: 90%;
font-size: 0.875em;
}
@media (min-width: 820px) {
.rule .offendersTable {
max-width: 90%;
font-size: 1em;
}
}
.rule .offendersTable > div {
display: table-row;
@@ -75,12 +84,12 @@
.rule .offendersTable > div > div {
display: table-cell;
background: #f2f2f2;
padding: 0 1em;
padding: 0 0.25em;
word-wrap: break-word;
word-break: break-all;
}
.rule .offendersTable > div > div:hover {
background: #EBD8E2;
background: #d8ebe0;
}
.rule .notFound {
font-size: 1em;
@@ -261,8 +270,8 @@
}
.smallPreview {
display: block;
max-height: 4em;
max-width: 8em;
max-height: 6em;
max-width: 16em;
border: 1px solid #000;
margin: 1em auto 0.2em;
}
+11 -1
View File
@@ -2,6 +2,16 @@
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
font-size: 1.2em;
margin-bottom: 0.5em;
}
@media (min-width: 420px) {
.screenshot .screenshotWrapper {
font-size: 1.6em;
}
}
@media (min-width: 640px) {
.screenshot .screenshotWrapper {
font-size: 2.08333333em;
}
}
-338
View File
@@ -1,338 +0,0 @@
/* 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,
.table > a {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div,
.table > a > 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;
}
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>

Before

Width:  |  Height:  |  Size: 634 B

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 792 B

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>

Before

Width:  |  Height:  |  Size: 679 B

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></path></svg>

Before

Width:  |  Height:  |  Size: 875 B

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 368.00,0.00L 144.00,0.00 c-26.40,0.00-48.00,21.60-48.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 224.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 416.00,48.00 C 416.00,21.60, 394.40,0.00, 368.00,0.00z M 192.00,24.00 l 128.00,0.00 l0.00,16.00 L 192.00,40.00 L 192.00,24.00 z M 256.00,480.00c-17.673,0.00-32.00-14.327-32.00-32.00s 14.327-32.00, 32.00-32.00s 32.00,14.327, 32.00,32.00S 273.673,480.00, 256.00,480.00z M 384.00,384.00L 128.00,384.00 L 128.00,64.00 l 256.00,0.00 L 384.00,384.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 851 B

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 512.00,416.00L 512.00,32.00 L0.00,32.00 l0.00,384.00 l 224.00,0.00 l0.00,32.00 l-96.00,0.00 l0.00,32.00 l 256.00,0.00 l0.00-32.00 l-96.00,0.00 l0.00-32.00 L 512.00,416.00 z M 64.00,96.00l 384.00,0.00 l0.00,256.00 L 64.00,352.00 L 64.00,96.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 584 B

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 400.00,0.00L 80.00,0.00 C 53.60,0.00, 32.00,21.60, 32.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 320.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 448.00,48.00 C 448.00,21.60, 426.40,0.00, 400.00,0.00z M 240.00,496.00 c-8.836,0.00-16.00-7.163-16.00-16.00s 7.164-16.00, 16.00-16.00s 16.00,7.163, 16.00,16.00S 248.836,496.00, 240.00,496.00z M 384.00,448.00L 96.00,448.00 L 96.00,64.00 l 288.00,0.00 L 384.00,448.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 774 B

-1
View File
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path><circle cx="256" cy="384" r="31" style="stroke:#000000; fill:#000000"/></svg>

Before

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 B

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+1 -6
View File
@@ -7,7 +7,6 @@ var yltApp = angular.module('YellowLabTools', [
'queueCtrl',
'ruleCtrl',
'screenshotCtrl',
'timelineCtrl',
'runsFactory',
'resultsFactory',
'apiService',
@@ -42,7 +41,7 @@ yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
// GitHub star button (asynchronously loaded iframe)
window.addEventListener('load', function() {
window.document.getElementById('ghbtn').src = 'https://ghbtns.com/github-btn.html?user=gmetais&repo=YellowLabTools&type=star&count=true&size=large';
window.document.getElementById('ghbtn').src = 'https://ghbtns.com/github-btn.html?user=YellowLabTools&repo=YellowLabTools&type=star&count=true&size=large';
});
}]);
@@ -64,10 +63,6 @@ yltApp.config(['$routeProvider', '$locationProvider',
templateUrl: 'views/dashboard.html',
controller: 'DashboardCtrl'
}).
when('/result/:runId/timeline', {
templateUrl: 'views/timeline.html',
controller: 'TimelineCtrl'
}).
when('/result/:runId/screenshot', {
templateUrl: 'views/screenshot.html',
controller: 'ScreenshotCtrl'
-23
View File
@@ -3,7 +3,6 @@ var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuServ
dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
$scope.fromSocialShare = $location.search().share;
function loadResults() {
// Load result if needed
@@ -34,27 +33,5 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
API.relaunchTest($scope.result);
};
// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
$scope.seeFullReport = function() {
$scope.fromSocialShare = false;
$location.search({});
};
$scope.shareOnTwitter = function(message) {
openSocialPopup('https://twitter.com/intent/tweet?text=' + encodeURIComponent(message + 'https://yellowlab.tools'));
};
$scope.shareOnLinkedin = function(message) {
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=https://yellowlab.tools&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
};
function openSocialPopup(url) {
var winHeight = 400;
var winWidth = 600;
var winTop = (screen.height / 2) - (winHeight / 2);
var winLeft = (screen.width / 2) - (winWidth / 2);
window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
}
loadResults();
}]);
+61 -3
View File
@@ -5,26 +5,50 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
var numberOfTries = 0;
var favicon = document.querySelector('link[rel=icon]');
var faviconUrl = 'img/favicon.png';
var faviconSuccessUrl = 'img/favicon-success.png';
var faviconFailUrl = 'img/favicon-fail.png';
var faviconInterval = null;
var faviconCounter = 0;
var faviconCanvas = null;
var faviconCanvasContext = null;
var faviconImage = null;
function getRunStatus () {
Runs.get({runId: $scope.runId}, function(data) {
$scope.url = data.params.url;
$scope.status = data.status;
$scope.progress = data.progress;
$scope.notFound = false;
$scope.connectionLost = false;
if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') {
if (data.status.statusCode === 'awaiting') {
numberOfTries ++;
rotateFavicon();
// Retrying in 2 seconds (and increasing the delay a bit more each time)
// Retrying every 2 seconds (and increasing the delay a bit more each time)
setTimeout(getRunStatus, 2000 + (numberOfTries * 100));
} else if (data.status.statusCode === 'running') {
numberOfTries ++;
rotateFavicon();
// Retrying every second or so
setTimeout(getRunStatus, 1000 + (numberOfTries * 10));
} else if (data.status.statusCode === 'complete') {
stopFavicon(true);
$location.path('/result/' + $scope.runId).replace();
} else {
// Handled by the view
stopFavicon(false);
// The rest is handled by the view
}
}, function(response) {
if (response.status === 404) {
stopFavicon(false);
$scope.notFound = true;
$scope.connectionLost = false;
} else if (response.status === 0) {
@@ -35,6 +59,40 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
}
});
}
function rotateFavicon() {
if (!faviconInterval) {
faviconImage = new Image();
faviconImage.onload = function() {
faviconCanvas = document.getElementById('faviconRotator');
faviconCanvasContext = faviconCanvas.getContext('2d');
faviconCanvasContext.fillStyle = '#212240';
if (!!faviconCanvasContext) {
faviconInterval = window.setInterval(faviconTick, 300);
}
};
faviconImage.src = faviconUrl;
}
}
function faviconTick() {
faviconCounter ++;
faviconCanvasContext.save();
faviconCanvasContext.fillRect(0, 0, 32, 32);
faviconCanvasContext.translate(16, 16);
faviconCanvasContext.rotate(22.5 * faviconCounter * Math.PI / 180);
faviconCanvasContext.translate(-16, -16);
faviconCanvasContext.drawImage(faviconImage, 0, 0, 32, 32);
faviconCanvasContext.restore();
favicon.href = faviconCanvas.toDataURL('image/png');
}
function stopFavicon(isSuccess) {
window.clearInterval(faviconInterval);
faviconInterval = null;
favicon.href = isSuccess ? faviconSuccessUrl : faviconFailUrl;
}
getRunStatus();
}]);
-62
View File
@@ -64,68 +64,6 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
}
};
}
// 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 = {
scales: {
xAxes: [{
gridLines: {
display:false
}
}],
yAxes: [{
gridLines: {
display:false
}
}]
},
tooltips: {
enabled: false
},
elements: {
point: {
radius: 0
}
}
};
}
}
$scope.backToDashboard = function() {
-190
View File
@@ -1,190 +0,0 @@
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.totalRequests.offendersObj.list.byType.js;
$scope.scripts = [];
offenders.forEach(function(filePath) {
var shortPath = filePath;
if (filePath.length > 100) {
shortPath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: filePath,
shortPath: shortPath
};
$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);
});
}
};
}]);
+11 -642
View File
@@ -93,510 +93,12 @@
};
});
function getJQueryContextButtonHTML(context, onASingleLine) {
if (context.length === 0) {
return '<span class="offenderButton">Empty jQuery object</span>';
}
if (context.length === 1) {
return getDomElementButtonHTML(context.elements[0], onASingleLine);
}
var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
if (context.length === 3) {
html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
} else if (context.length > 3) {
html += ' and ' + (context.length - 2) + ' more...';
}
return html + ')';
}
function isJQuery(node) {
return node.data.type.indexOf('jQuery ') === 0;
}
function getNonJQueryHTML(node, onASingleLine) {
var type = node.data.type;
if (node.windowPerformance) {
switch (type) {
case 'documentScroll':
return '(triggering the scroll event on <b>document</b>)';
case 'windowScroll':
return '(triggering the scroll event on <b>window</b>)';
case 'window.onscroll':
return '(calling the <b>window.onscroll</b> function)';
default:
return '';
}
}
if (!node.data.callDetails) {
return '';
}
var args = node.data.callDetails.arguments;
var ctxt = node.data.callDetails.context;
switch (type) {
case 'getElementById':
case 'createElement':
return '<b>' + args[0] + '</b>';
case 'getElementsByClassName':
case 'getElementsByTagName':
case 'querySelector':
case 'querySelectorAll':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'appendChild':
return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'insertBefore':
return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
case 'addEventListener':
return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'getComputedStyle':
return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || '');
case 'error':
return args[0];
case 'jQuery - onDOMReady':
return '(function)';
case 'documentScroll':
return 'The scroll event just triggered on document';
case 'windowScroll':
return 'The scroll event just triggered on window';
case 'window.onscroll':
return 'The window.onscroll function just got called';
default:
return '';
}
}
function getJQueryHTML(node, onASingleLine) {
var type = node.data.type;
var unescapedArgs = node.data.callDetails.arguments;
var args = [];
var ctxt = node.data.callDetails.context;
// escape HTML in args
for (var i = 0 ; i < 4 ; i ++) {
if (unescapedArgs[i] !== undefined) {
args[i] = escapeHTML(unescapedArgs[i]);
}
}
if (type === 'jQuery loaded' || type === 'jQuery version change') {
return args[0];
}
switch (type) {
case 'jQuery - onDOMReady':
case 'jQuery - windowOnLoad':
return '(function)';
case 'jQuery - Sizzle call':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'jQuery - find':
if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return '<b>' + args[0] + '</b>';
}
break;
case 'jQuery - html':
if (args[0] !== undefined) {
return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - append':
return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - appendTo':
return 'append ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - prepend':
return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prependTo':
return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - before':
return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertBefore':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
case 'jQuery - after':
return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertAfter':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
case 'jQuery - remove':
case 'jQuery - detach':
if (args[0]) {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - empty':
case 'jQuery - clone':
case 'jQuery - unwrap':
case 'jQuery - show':
case 'jQuery - hide':
case 'jQuery - animate':
case 'jQuery - fadeIn':
case 'jQuery - fadeOut':
case 'jQuery - fadeTo':
case 'jQuery - fadeToggle':
case 'jQuery - slideDown':
case 'jQuery - slideUp':
case 'jQuery - slideToggle':
return getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - replaceWith':
return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
case 'jQuery - replaceAll':
return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - text':
if (args[0] !== undefined) {
return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - wrap':
case 'jQuery - wrapAll':
return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - wrapInner':
return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - css':
case 'jQuery - attr':
case 'jQuery - prop':
if (isStringOfObject(args[0])) {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else if (args[1]) {
return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offset':
case 'jQuery - height':
case 'jQuery - innerHeight':
case 'jQuery - width':
case 'jQuery - innerWidth':
case 'jQuery - scrollLeft':
case 'jQuery - scrollTop':
case 'jQuery - position':
if (args[0]) {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - outerHeight':
case 'jQuery - outerWidth':
if (args[0] && args[0] !== 'true') {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else if (args[0] === 'true') {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)';
} else {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - toggle':
if (args[0] === 'true') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
} else if (args[0] === 'false') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - on':
case 'jQuery - one':
if (isStringOfObject(args[0])) {
return '<b>' + args[0].replace(/&quot;\(function\)&quot;/g, '(function)') + '</b>';
} else if (args[1] && isPureString(args[1])) {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - off':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - live':
case 'jQuery - bind':
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - die':
case 'jQuery - unbind':
if (args[0]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'unbind all events';
}
break;
case 'jQuery - delegate':
return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
case 'jQuery - undelegate':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
} else {
return 'unbind namespace <b>' + args[0] + '</b>';
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - blur':
case 'jQuery - change':
case 'jQuery - click':
case 'jQuery - dblclick':
case 'jQuery - focus':
case 'jQuery - keydown':
case 'jQuery - keypress':
case 'jQuery - keyup':
case 'jQuery - mousedown':
case 'jQuery - mouseenter':
case 'jQuery - mouseleave':
case 'jQuery - mousemove':
case 'jQuery - mouseout':
case 'jQuery - mouseover':
case 'jQuery - mouseup':
case 'jQuery - resize':
case 'jQuery - scroll':
case 'jQuery - select':
case 'jQuery - submit':
if (args[0]) {
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - error':
case 'jQuery - focusin':
case 'jQuery - focusout':
case 'jQuery - hover':
case 'jQuery - load':
case 'jQuery - unload':
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - removeAttr':
case 'jQuery - removeProp':
return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - val':
if (args[0]) {
return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - hasClass':
case 'jQuery - addClass':
case 'jQuery - removeClass':
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - toggleClass':
if (args[0]) {
if (args[1]) {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
} else {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'magic no-argument toggleClass';
}
break;
case 'jQuery - children':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - closest':
if (args[1]) {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
} else {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - next':
case 'jQuery - nextAll':
if (args[0]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - nextUntil':
if (args[0]) {
if (args[1]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offsetParent':
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prev':
case 'jQuery - prevAll':
if (args[0]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - prevUntil':
if (args[0]) {
if (args[1]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parent':
case 'jQuery - parents':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parentsUntil':
if (args[0]) {
if (args[1]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - siblings':
if (args[0]) {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
default:
return '';
}
}
function escapeHTML(html) {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
offendersDirectives.filter('lastDOMNode', function() {
return function(str) {
var splited = str.split(' > ');
return splited[splited.length - 1];
};
return String(html).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
function joinArgs(args) {
var html = '<b>' + args[0] + '</b>';
if (args[1]) {
html += ', <b>' + args[1] + '</b>';
if (args[2]) {
html += ', <b>' + args[2] + '</b>';
if (args[3]) {
html += ', and more...';
}
}
}
return html;
}
function isStringOfObject(str) {
return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
}
function getTimelineParamsHTML(node, onASingleLine) {
if (isJQuery(node)) {
return getJQueryHTML(node, onASingleLine);
} else {
return getNonJQueryHTML(node, onASingleLine);
}
}
});
function getBacktraceHTML(backtrace) {
var html = '';
@@ -687,145 +189,6 @@
return out;
}
function getTimelineDetailsHTML(node) {
var html = '';
if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
if (node.warning || node.error) {
html += '<div class="icon-warning"></div>';
} else {
html += '<div class="icon-question"></div>';
}
html += '<div class="detailsOverlay">';
html += '<div class="closeBtn">✖</div>';
if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
} else if (node.eventNotDelegated) {
html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
}
if (node.data.resultsNumber === 0) {
html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
} else if (node.data.resultsNumber > 0) {
html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
}
if (node.data.backtrace) {
html += '<h4>Backtrace</h4>';
html += '<div class="table">';
html += getBacktraceHTML(node.data.backtrace);
html += '</div>';
}
html += '</div>';
}
return html;
}
offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
var numberWithCommas = $filter('number');
function getProfilerLineHTML(index, node) {
return '<div class="index">' + (index + 1) + '</div>' +
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
'<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
}
function recursiveChildrenHTML(node) {
var html = '';
if (node.children) {
node.children.forEach(function(child) {
html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
});
}
return html;
}
function onDetailsClick(row) {
// Close if it's already open
if (row.classList.contains('showDetails')) {
closeDetails(row);
return;
}
// Close any other open details overlay
var openOnes = document.getElementsByClassName('showDetails');
if (openOnes.length > 0) {
openOnes[0].classList.remove('showDetails');
}
// Make it appear
row.classList.add('showDetails');
// Bind the close button
row.querySelector('.closeBtn').addEventListener('click', function() {
closeDetails(row);
});
}
function closeDetails(row) {
row.classList.remove('showDetails');
// Unbind the close button
row.querySelector('.closeBtn').removeEventListener('click', closeDetails);
}
return {
restrict: 'E',
scope: {
index: '=',
node: '='
},
template: '<div></div>',
replace: true,
link: function(scope, element) {
if (scope.node.error) {
element.addClass('jsError');
} else if (scope.node.windowPerformance) {
element.addClass('windowPerformance');
}
element.append(getProfilerLineHTML(scope.index, scope.node));
element[0].id = 'line_' + scope.index;
if (scope.node.warning) {
element[0].classList.add('warning');
if (scope.node.queryWithoutResults) {
element[0].classList.add('queryWithoutResults');
}
if (scope.node.jQueryCallOnEmptyObject) {
element[0].classList.add('jQueryCallOnEmptyObject');
}
if (scope.node.eventNotDelegated) {
element[0].classList.add('eventNotDelegated');
}
}
// Bind click on the details icon
var detailsIcon = element[0].querySelector('.details div');
if (detailsIcon) {
detailsIcon.addEventListener('click', function() {
onDetailsClick(this.parentNode.parentNode);
});
}
}
};
}]);
function shortenUrl(url, maxLength) {
if (!maxLength) {
maxLength = 110;
@@ -935,4 +298,10 @@
};
});
offendersDirectives.filter('roundNbr', function() {
return function(nbr) {
return Math.round(nbr);
};
});
})();
+2 -2
View File
@@ -21,9 +21,9 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
};
if (settings.domainsBlackOrWhite === 'black') {
if (settings.domainsBlockOrAllow === 'block') {
runObject.blockDomain = this.parseDomains(settings.domains);
} else if (settings.domainsBlackOrWhite === 'white') {
} else if (settings.domainsBlockOrAllow === 'allow') {
var allowedDomains = this.parseDomains(settings.domains);
if (allowedDomains.length > 0) {
runObject.allowDomain = allowedDomains;
-3
View File
@@ -22,9 +22,6 @@ menuService.factory('Menu', ['$location', function($location) {
case 'dashboard':
$location.path('/result/' + currentRunId);
break;
case 'timeline':
$location.path('/result/' + currentRunId + '/timeline');
break;
default:
console.err('Undefined Menu.changePage() destination');
}
+1 -1
View File
@@ -6,7 +6,7 @@ settingsService.factory('Settings', ['localStorageService', function(localStorag
getMergedSettings: function() {
var defaultSettings = {
device: 'desktop',
device: 'phone',
showAdvanced: false
};
+10 -2
View File
@@ -1,6 +1,10 @@
.about {
margin: 3em auto;
width: 50%;
width: 80%;
@media (min-width: 640px) {
width: 50%;
}
}
.about p {
@@ -8,5 +12,9 @@
}
.about a {
color: #FFF;
color: #fff;
}
.sponsor {
color: #ffa319;
}
+69 -62
View File
@@ -7,16 +7,8 @@
}
.summary .globalScore {
display: table;
width: 60%;
margin: 3em auto;
> div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.globalGrade {
margin: 0.5 auto;
width: 2.5em;
@@ -24,12 +16,10 @@
line-height: 2.5em;
border-radius: 0.5em;
font-size: 3em;
font-weight: bold;
vertical-align: middle;
}
.on100 {
font-size: 1.2em;
font-weight: bold;
margin: 0.5em 0 1em;
}
@@ -61,40 +51,77 @@
top: 1.5em;
left: 0.9em;
}
@media (min-width: 820px) {
width: 65%;
display: table;
> div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
}
}
.summary .notations {
width: 100%;
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
margin: 0 0 1.5em;
border-spacing: 0 1em;
@media (min-width: 820px) {
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;
@media (min-width: 820px) {
display: table-cell;
height: 2.5em;
}
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
font-size: 1.2em;
width: 50%;
float: left;
text-align: left;
margin: 0.5em 0.25em;
@media (min-width: 820px) {
width: 20%;
text-align: center;
float: none;
}
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
@media (min-width: 820px) {
width: 75%;
}
}
.A, .B, .C, .D, .E, .F, .NA {
.summary .notations &.categoryScore {
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
margin: 0.2em;
font-size: 1.5em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
float: right;
@media (min-width: 820px) {
float: none;
font-size: 2em;
}
}
.summary .notations .grade & {
width: 1em;
@@ -113,12 +140,12 @@
color: inherit;
}
> a:hover > div {
background: #EBD8E2;
background: #d8ebe0;
cursor: pointer;
&.info {
background: #FFF;
.icon-question {
color: #EBD8E2;
svg {
fill: #d8ebe0;
}
}
}
@@ -134,58 +161,38 @@
}
.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 {
.summary .notations .warning .label, .summary .notations .warning .result {
color: #FF1919;
}
.summary .notations .icon-warning svg {
fill: #FF1919;
margin: -2px 0;
}
.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;
}
display: none;
.summary .fromShare {
margin-bottom: 3em;
@media (min-width: 820px) {
display: table-cell;
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
}
.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;
}
.tweetButton, .linkedinButton {
color: #413;
background: #F2F2F2;
margin-right: 0;
&:hover {
color: #F2F2F2;
background: #e74c3c;
}
}
input {
font-size: 0.9em;
}
.summary .notations .criteria .info svg {
fill: transparent;
}
.summary .sponsor {
font-size: 0.9em;
margin-bottom: 4em;
color: #413;
color: #ffa319;
a {
color: inherit;
}
File diff suppressed because one or more lines are too long
+29 -12
View File
@@ -22,7 +22,7 @@
background: #e74c3c;
}
&.disabled {
background: #deaca6;
background: #f1bd70;
&:focus {
color: #ddd;
}
@@ -59,27 +59,35 @@
text-decoration: none;
font-size: 0.8em;
> svg {
display: block;
margin: 0.6em auto 0.3em;
fill: #fff;
}
&.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
> svg {
fill: #ffa319;
}
}
&:hover {
color: #ffa319;
}
> div {
margin: 0.2em 0 0.1em;
font-size: 3em;
> svg {
fill: #ffa319;
}
}
}
}
.settingsTooltip {
position: relative;
span {
font-size: 0.8em;
svg {
vertical-align: text-top;
}
@@ -94,6 +102,8 @@
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
word-break: break-all;
word-break: break-word;
z-index: 2;
}
@@ -171,12 +181,20 @@
width: 50%;
margin: 6em auto 0;
font-size: 0.9em;
color: #413;
color: #8abfaf;
> div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
@media (min-width: 640px) {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
}
h3 {
font-size: 1.5em;
font-weight: normal;
color: #fff;
}
}
@@ -187,7 +205,6 @@ input[type=submit], input.url {
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
+64 -26
View File
@@ -1,13 +1,14 @@
@import "icons.less";
html {
margin: 100px 50px;
margin: 35px 5px;
@media (min-width: 640px) {
margin: 100px 50px;
}
}
body {
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
background: #212240;
color: #fff;
font-size: 16px;
text-align: center;
@@ -21,41 +22,61 @@ input[type=submit] {
cursor: pointer;
}
h1 span {
display: inline-block;
height: 1em;
width: 1em;
color: #ffa319;
h1 {
font-weight: 200;
}
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
font-size: 0.8em;
display: inline-block;
margin: 1em;
width: 8em;
width: 7em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
@media (min-width: 640px) {
font-size: 1em;
margin: 1em;
width: 8em;
border: 2px solid #fff;
border-radius: 0.5em;
}
svg {
fill: #fff;
}
&.back, &.restart {
color: #413;
border-color: #413;
color: #fff;
border-color: #fff;
}
}
.resultsMenu .menuItem div {
padding-top: 0.5em;
font-size: 3em;
}
.resultsMenu svg {
display: block;
margin: 1.2em auto 0.2em;
}
.resultsMenu .active, .resultsMenu .menuItem.active:hover {
color: #ffa319;
border-color: #ffa319;
svg {
fill: #ffa319;
}
}
.resultsMenu .menuItem:hover {
color: #ffa319;
svg {
fill: #ffa319;
}
}
.resultsMenu span {
position: relative;
@@ -134,10 +155,6 @@ a.linkButton {
> div {
position: relative;
overflow: hidden;
&:hover {
overflow: auto;
}
}
.screenshotImage {
@@ -149,7 +166,7 @@ a.linkButton {
}
}
.screenshotWrapper.desktop {
.screenshotWrapper.desktop, .screenshotWrapper.desktop-hd {
border: 0.2em solid #AAA;
padding: 0.5em;
border-top-left-radius: 0.4em;
@@ -179,7 +196,7 @@ a.linkButton {
> div {
width: 12em;
height: 6.75em;
height: 7.5em;
}
}
@@ -233,14 +250,35 @@ a.linkButton {
}
> div {
width: 8.25em;
height: 11em;
width: 8em;
height: 12.8em;
}
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div,
.table > a {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div,
.table > a > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.footer {
padding: 3em;
color: #413;
color: #fff;
a {
color: inherit;
}
@@ -255,6 +293,6 @@ a.linkButton {
}
}
[class^="icon-"]:before, [class*=" icon-"]:before {
vertical-align: baseline;
.homeSponsor {
color: #ffa319;
}
+15
View File
@@ -6,4 +6,19 @@
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
.progressBarEmpty {
width: 90%;
max-width: 300px;
margin: 1em auto;
padding: 0.05em;
border: 1px solid #ffa319;
}
.progressBarFilled {
width: 5%;
height: 0.5em;
background: #ffa319;
transition: width 3s ease-out;
}
+27 -17
View File
@@ -3,23 +3,27 @@
}
.rule .ruleTable {
display: table;
border-spacing: 1em;
width: 90%;
margin: 2em auto;
background: #f2f2f2;
border: 1px dashed #666;
border-radius: 0.5em;
> div {
display: table-cell;
vertical-align: middle;
}
.left {
width: 33%;
font-weight: bold;
}
.right {
width: 67%;
@media (min-width: 820px) {
display: table;
> div {
display: table-cell;
vertical-align: middle;
}
.left {
width: 33%;
}
.right {
width: 67%;
}
}
}
@@ -29,7 +33,7 @@
height: 2em;
width: 2em;
border-radius: 0.5em;
margin: 0 auto 0.5em;
margin: 0 auto 0.25em;
}
.rule h3 {
@@ -75,17 +79,23 @@
border-spacing: 0 0.25em;
margin: 0 auto;
min-width: 10%;
max-width: 90%;
font-size: 0.875em;
@media (min-width: 820px) {
max-width: 90%;
font-size: 1em;
}
> div {
display: table-row;
> div {
display: table-cell;
background: #f2f2f2;
padding: 0 1em;
padding: 0 0.25em;
word-wrap: break-word;
word-break: break-all;
&:hover {
background: #EBD8E2;
background: #d8ebe0;
}
}
}
@@ -290,8 +300,8 @@
.smallPreview {
display: block;
max-height: 4em;
max-width: 8em;
max-height: 6em;
max-width: 16em;
border: 1px solid #000;
margin: 1em auto 0.2em;
}
+9 -1
View File
@@ -3,6 +3,14 @@
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
font-size: 1.2em;
margin-bottom: 0.5em;
@media (min-width: 420px) {
font-size: 1.6em;
}
@media (min-width: 640px) {
font-size: 2.08333333333333em;
}
}
-387
View File
@@ -1,387 +0,0 @@
/* Timeline colors, related to Window Performances */
@domCreationColor: #FF6600;
@domCreationBg: #FFE0CC;
@domContentLoadedColor: #A7E846;
@domContentLoadedBg: #E0FFD1;
@domContentLoadedEndColor: #7ECCCC;
@domContentLoadedEndBg: #D8F0F0;
@domCompleteColor: #C2A3FF;
@domCompleteBg: #EDE3FF;
@domInteractiveColor: #FFE433;
@domInteractiveBg: #FFFCCC;
.execution {
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
select {
max-width: 30em;
}
&.empty {
font-size: 0.8em;
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%;
&.clickable {
cursor: pointer;
}
}
.timeline div.interval:hover {
background: #9C4274;
}
.timeline .interval:hover .color {
background: #F04DA7;
}
.timeline .domComplete.interval {
background: @domCompleteBg;
}
.timeline .domComplete .color {
background: @domCompleteColor;
}
.timeline .domContentLoadedEnd.interval {
background: @domContentLoadedEndBg;
}
.timeline .domContentLoadedEnd .color {
background: @domContentLoadedEndColor;
}
.timeline .domContentLoaded.interval {
background: @domContentLoadedBg;
}
.timeline .domContentLoaded .color {
background: @domContentLoadedColor;
}
.timeline .domInteractive.interval {
background: @domInteractiveBg;
}
.timeline .domInteractive .color {
background: @domInteractiveColor;
}
.timeline .domCreation.interval {
background: @domCreationBg;
}
.timeline .domCreation .color {
background: @domCreationColor;
}
.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,
.table > a {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div,
.table > a > 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;
> 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;
> .child {
margin-left: 1em;
}
&:before {
content: "↳";
}
.childArgs {
display: none;
}
span {
position: relative;
}
span:hover {
background: #EBD8E2;
div {
display: inline-block;
}
.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;
.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: @domCompleteBg;
}
.table > div > .startTime.domContentLoadedEnd {
background: @domContentLoadedEndBg;
}
.table > div > .startTime.domContentLoaded {
background: @domContentLoadedBg;
}
.table > div > .startTime.domInteractive {
background: @domInteractiveBg;
}
.table > div > .startTime.domCreation {
background: @domCreationBg;
}
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.queryWithoutResultsFilterOn {
> div {
display: none;
&.queryWithoutResults {
display: table-row;
}
}
}
.jQueryCallOnEmptyObjectFilterOn {
> div {
display: none;
&.jQueryCallOnEmptyObject {
display: table-row;
}
}
}
.eventNotDelegatedFilterOn {
> div {
display: none;
&.eventNotDelegated {
display: table-row;
}
}
}
.jsErrorFilterOn {
> div {
display: none;
&.jsError {
display: table-row;
}
}
}
+8 -11
View File
@@ -1,11 +1,12 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools</title>
<title>Yellow Lab Tools - Page Speed audit</title>
<base href="<%= baseUrl %>">
<link rel="icon" type="image/png" href="img/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta property="og:image" content="img/logo-large.png" />
<meta name="description" content="Free online web performance tool. Audit your webpage for performance and front-end quality issues. And it's open-source!" />
<meta name="description" content="Yellow Lab Tools is a free online web performance analyzer. It audits a webpage for performance and front-end quality issues. And it's open-source!" />
<!-- build:css css/styles.css-->
<link rel="stylesheet" type="text/css" href="css/main.css">
@@ -14,26 +15,22 @@
<link rel="stylesheet" type="text/css" href="css/queue.css">
<link rel="stylesheet" type="text/css" href="css/rule.css">
<link rel="stylesheet" type="text/css" href="css/screenshot.css">
<link rel="stylesheet" type="text/css" href="css/timeline.css">
<link rel="stylesheet" type="text/css" href="css/about.css">
<!-- endbuild -->
<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://ghbtns.com">
<link rel="dns-prefetch" href="https://ghbtns.com">
<link rel="preconnect" href="https://api.github.com">
<link rel="dns-prefetch" href="https://api.github.com">
</head>
<body ng-app="YellowLabTools">
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
<div id="header"><h1>Yellow Lab <svg width="32" height="32" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="#ffa319"><path d="M478 402L320 139V32h16c9 0 16-7 16-16s-7-16-16-16H176c-9 0-16 7-16 16s7 16 16 16h16v107L34 402c-36 61-8 110 62 110h320c70 0 98-49 62-110zm-357-82l103-172V32h64v116l103 172H121z"/></svg> Tools</h1></div>
<div id="body" ng-view autoscroll="true"></div>
<div class="footer">
<span class="version">@@version</span>
<span class="version" id="version">@@version</span>
<br><a href="<%= baseUrl %>about">More about Yellow Lab Tools</a><br>
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="160px" height="30px"></iframe></div>
</div>
<!-- build:js js/all.js -->
@@ -51,7 +48,6 @@
<script src="js/controllers/queueCtrl.js"></script>
<script src="js/controllers/ruleCtrl.js"></script>
<script src="js/controllers/screenshotCtrl.js"></script>
<script src="js/controllers/timelineCtrl.js"></script>
<script src="js/models/resultsFactory.js"></script>
<script src="js/models/runsFactory.js"></script>
<script src="js/services/apiService.js"></script>
@@ -63,7 +59,8 @@
<!-- endbuild -->
<script>
if('@@googleAnalyticsId'.indexOf('UA-')===0){(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');ga('create','@@googleAnalyticsId','auto');}
document.getElementById('version').innerHTML = "<%= version %>";
if('<%= googleAnalyticsId %>'.indexOf('UA-')===0){(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');ga('create','<%= googleAnalyticsId %>','auto');}
</script>
</body>
</html>
+3 -3
View File
@@ -1,9 +1,9 @@
<div class="about">
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>. It allows you to test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
<p><b>Yellow Lab Tools</b> is an open source project by <a href="https://letstalkaboutwebperf.com/en/" target="_blank">Gaël Métais</a>. It allows you to test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
<p>This is done by loading the webpage via PhantomJS and collecting various metrics and statistics with the help of <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>. These metrics are categorized and transformed into scores. It also gives in-depth details so that developers can correct the detected issues.</p>
<p>It is based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>, a tool that instruments Chrome Headless to collect dozens of metrics. These metrics are then categorized and transformed into scores. It also provides in-depth details so that developers can fix the detected issues.</p>
<p>By the way, <b>it's free</b> because we are geeks, not businessmen. All we want is <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">a <span>&#9733;</span> on GitHub</a>. It will boost our motivation to add more awesome features!!!</p>
<p>By the way, <b>it's entirely free</b>. In return, you can add <a href="https://github.com/YellowLabTools/YellowLabTools" target="_blank">a <span>&#9733;</span> on GitHub</a> or <a href="https://ko-fi.com/gaelmetais" target="_blank">buy me a coffee</a>. It will boost my motivation to add more awesome features!</p>
<%if (sponsoring.about) { %>
<div class="sponsor"><%- sponsoring.about %></div>
+14 -22
View File
@@ -18,7 +18,7 @@
</div>
<div>
<a href="result/{{result.runId}}/screenshot">
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<div class="screenshotWrapper" ng-class="result.params.options.device || 'phone'">
<div>
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
@@ -43,36 +43,28 @@
<div class="label">{{rule.policy.label}}</div>
<div class="result">
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
<span ng-if="rule.abnormal" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}} <span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
<span ng-if="rule.abnormal" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
</div>
<div class="info"><span class="icon-question"></span></div>
<div class="info"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg></div>
</a>
</div>
</div>
</div>
</div>
<%if (sponsoring.dashboard) { %>
<div ng-if="!error && !fromSocialShare" class="sponsor"><%- sponsoring.dashboard %></div>
<%if (sponsoring.wordpress) { %>
<div ng-if="result.frameworks.isWordPress && !error" class="sponsor"><%- sponsoring.wordpress %></div>
<% } %>
<%if (sponsoring.dashboard) { %>
<div ng-if="!error" class="sponsor"><%- sponsoring.dashboard %></div>
<% } %>
<div class="tweet" ng-if="!error && !fromSocialShare">
<form ng-submit="sendTweet()">
<input type="text" class="tweetText" ng-model="tweetText">
<input type="submit" class="tweetButton" value="Tweet this!" ng-click="shareOnTwitter(tweetText)">
<input type="submit" class="linkedinButton" value="LinkedIn this!" ng-click="shareOnLinkedin(tweetText)">
</form>
</div>
<div class="fromShare" ng-if="!error && fromSocialShare">
<p>Yellow Lab Tools is a free online tool that analyzes performance and front-end quality.</p>
<a class="linkButton" href="" ng-click="seeFullReport()">See the full report for this page</a>
<a class="linkButton" href="<%= baseUrl %>">Test another webpage</a>
</div>
<div ng-if="error">
<h2>Run failed / Run not found</h2>
+19 -18
View File
@@ -2,14 +2,15 @@
<p class="price">Free and open source!</p>
<form ng-submit="launchTest()" >
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
<input type="text" name="url" ng-model="url" placeholder="https://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
<div class="settings">
<div class="device">
<div>Choose the simulated device:</div>
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
<a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
<a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M368 0H144c-26 0-48 22-48 48v416c0 26 22 48 48 48h224c26 0 48-22 48-48V48c0-26-22-48-48-48zM192 24h128v16H192V24zm64 456a32 32 0 110-64 32 32 0 010 64zm128-96H128V64h256v320z"/></svg>Phone</a>
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M400 0H80C54 0 32 22 32 48v416c0 26 22 48 48 48h320c26 0 48-22 48-48V48c0-26-22-48-48-48zM240 496a16 16 0 110-32 16 16 0 010 32zm144-48H96V64h288v384z"/></svg>Tablet</a>
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M512 416V32H0v384h224v32h-96v32h256v-32h-96v-32h224zM64 96h384v256H64V96z"/></svg>Desktop</a>
<a href="" class="item" ng-class="{active: settings.device == 'desktop-hd'}" ng-click="settings.device = 'desktop-hd'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M512 416V32H0v384h224v32h-96v32h256v-32h-96v-32zM64 96h384v256H64z"/><path d="M270 297V161h28c14 0 25 2 33 4 8 3 16 7 23 14 14 13 21 29 21 50s-7 38-22 51c-7 6-15 11-23 13-8 3-18 4-32 4zm20-19h10c9 0 16-1 23-3a47 47 0 0031-46c0-15-5-27-15-36-9-8-22-12-39-12h-10zm-123-64h59v-53h20v136h-20v-63h-59v63h-20V161h20z"/></svg>Desktop</a>
</div>
[ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
<span ng-if="!settings.showAdvanced">Advanced settings &nbsp;</span>
@@ -28,7 +29,7 @@
<div class="label">
Wait selector
<span class="settingsTooltip">
<span class="icon-question"></span>
<svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
<div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
</span>
</div>
@@ -38,7 +39,7 @@
<div class="label">
Cookie
<span class="settingsTooltip">
<span class="icon-question"></span>
<svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
<div><b>Cookie</b><br><br>Adds cookies, separated by a pipe character.<br><br>Example: "bar1=foo1;domain=.domain1.com|bar2=foo2;domain=www.domain2.com"</div>
</span>
</div>
@@ -48,7 +49,7 @@
<div class="label">
Authent
<span class="settingsTooltip">
<span class="icon-question"></span>
<svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
<div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
</span>
</div>
@@ -67,7 +68,7 @@
<div class="label">
HTTP proxy
<span class="settingsTooltip">
<span class="icon-question"></span>
<svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
<div><b>HTTP proxy</b><br><br>Insert here your proxy settings with the format "host:port".<br><br>Example: "192.168.10.0:3333"</div>
</span>
</div>
@@ -77,14 +78,14 @@
<div class="label">
Block domains
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
<svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty allow list will block all domains except the main domain.</div>
</span>
</div>
<div>
<div>
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
<input type="radio" name="blockOrAllow" ng-model="settings.domainsBlockOrAllow" value="block" />block list
<input type="radio" name="blockOrAllow" ng-model="settings.domainsBlockOrAllow" value="allow" />allow list
</div>
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
</div>
@@ -96,18 +97,18 @@
<div class="features">
<div>
<h3>Front-end quality</h3>
<p>Audits quality problems in the HTML, CSS and JS</p>
<h3>Page speed audit</h3>
<p>Checks if performance good practices are respected</p>
</div>
<div>
<h3>WebPerf</h3>
<p>Checks if performance good practices are applied</p>
<h3>Front-end analyzis</h3>
<p>Detects problems on HTML, CSS, JS, images, fonts and more</p>
</div>
<div>
<h3>JS Profiling</h3>
<p>Untangles the JavaScript spaghetti code</p>
<h3>In-depth details</h3>
<p>Provides precise information to fix the detected performance issues</p>
</div>
</div>
+13 -3
View File
@@ -5,7 +5,7 @@
<div class="status">Test failed</div>
<p class="statusSubMessage">{{status.error}}</p>
<a class="linkButton" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
<a class="linkButton" href="https://github.com/YellowLabTools/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
<a class="linkButton" href="<%= baseUrl %>">New test</a>
</div>
<div ng-if="status.statusCode == 'awaiting'">
@@ -17,7 +17,16 @@
</div>
<div ng-if="status.statusCode == 'running'">
<div class="status">Test is running...</div>
<p class="statusSubMessage">(auto-refresh activated)</p>
<div class="progress">
<div class="progressBarEmpty">
<div class="progressBarFilled" ng-style="{'width': (progress.estimatedProgress*100) + '%'}"></div>
</div>
</div>
<p class="statusSubMessage" ng-if="!progress">(Phantomas launched)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'domReady'">(DOM Ready fired)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'domComplete'">(page loaded, waiting for late requests)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'phantomas'">(now simulating compression, optimization and minification)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'redownload'">(calculating score and retrieving screenshot)</p>
</div>
<div ng-if="status.statusCode == 'complete'">
<div class="status">Test complete</div>
@@ -33,4 +42,5 @@
<div ng-if="connectionLost == true">
<div class="status">Connection lost with server</div>
<p class="statusSubMessage">Check your wifi cable, or maybe YellowLab.tools is rebooting.</p>
</div>
</div>
<canvas id="faviconRotator" hidden width=32 height=32></canvas>
+3 -4
View File
@@ -1,8 +1,7 @@
<div>Tested url: &nbsp; <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
<div class="resultsMenu">
<a class="menuItem back" href="<%= baseUrl %>"><div class="icon-arrow-left3"></div><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
<a class="menuItem back" href="<%= baseUrl %>"><svg width="48" height="48" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 0a256 256 0 110 512 256 256 0 010-512zm0 464a208 208 0 100-416 208 208 0 000 416zM105 233l128-128a32 32 0 1146 46l-74 73h179a32 32 0 010 64H205l74 73a32 32 0 01-46 46L105 279a32 32 0 010-46z"/></svg><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><svg width="48" height="48" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M437 75a255 255 0 00-421 91l60 23a192 192 0 01316-69l-72 72h192V0l-75 75zM256 448c-53 0-101-21-136-56l72-72H0v192l75-75a255 255 0 00421-91l-60-23c-27 73-98 125-180 125z"/></svg><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><svg width="48" height="48" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h128v128H0zm192 32h320v64H192zM0 192h128v128H0zm192 32h320v64H192zM0 384h128v128H0zm192 32h320v64H192z"/></svg><span>Dashboard</span></div>
</div>
+129 -100
View File
@@ -12,15 +12,27 @@
<h3>
Value:
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}<span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
</h3>
<div class="okThreshold" ng-if="rule.score < 100 && rule.policy.isOkThreshold !== undefined">
Have
<span ng-if="rule.policy.unit == 'bytes'">{{rule.policy.isOkThreshold | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}<span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
<span ng-if="rule.policy.isOkThreshold > 0 && rule.policy.isOkThreshold < rule.policy.isBadThreshold">or less</span>
<span ng-if="rule.policy.isOkThreshold > rule.policy.isBadThreshold">or more</span>
to get the 100/100 score.
to get the 100/100 score on this issue.
</div>
<div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed > 0 && result.scoreProfiles.generic.globalScore >= 0">
Your new global score would increase by {{rule.globalScoreIfFixed - result.scoreProfiles.generic.globalScore}} points ({{rule.globalScoreIfFixed}}/100).
</div>
<div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed > 0 && result.scoreProfiles.generic.globalScore < 0">
Your new global score would increase by {{rule.globalScoreIfFixed}} points ({{rule.globalScoreIfFixed}}/100).
</div>
<div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed <= 0">
Your new global score would increase, but still not enough to reach 0/100. That's embarassing...
</div>
<div class="okThreshold" ng-if="rule.globalScoreIfFixed == result.scoreProfiles.generic.globalScore && rule.score < 100">
Your new global score would slightly increase, but not enough to gain a single point.
</div>
<div ng-bind-html="rule.policy.message" class="message"></div>
</div>
@@ -30,7 +42,7 @@
<p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
</div>
<div class="offenders" ng-if="rule.policy.hasOffenders">
<h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offender', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<div ng-if="rule.offendersObj.list" class="offendersTable">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
@@ -39,8 +51,13 @@
</div>
<div ng-if="!offender.parseError">
<div ng-if="policyName === 'iframesCount'">
<span ng-if="offender.url">{{offender.url}}</span>
<span ng-if="!offender.url">an iframe without URL</span>
</div>
<div ng-if="policyName === 'DOMidDuplicated'">
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
<b>{{offender.id}}</b>: {{offender.count}} occurrences
</div>
<div ng-if="policyName === 'DOMqueriesAvoidable'">
@@ -97,9 +114,9 @@
</div>
<div ng-if="policyName === 'cssRules'">
<span ng-if="offender.file === 'inline CSS'">inline CSS</span>
<span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
: <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
<span ng-if="offender.url === '[inline CSS]'">inline CSS</span>
<span ng-if="offender.url !== '[inline CSS]'"><url-link url="offender.url" max-length="80"></url-link></span>
: <ng-pluralize count="offender.value" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
</div>
<div ng-if="policyName === 'similarColors'">
@@ -118,7 +135,7 @@
</div>
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
<b>{{offender.property}} {{offender.message}}</b>
<b>{{offender.property}}</b> {{offender.message}}
<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>
@@ -132,17 +149,28 @@
</div>
</div>
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold'">
<img ng-src="{{offender.url | https}}" class="smallPreview checker"></img>
<url-link url="offender.url" max-length="70"></url-link> (offset: {{offender.offset | roundNbr}}px)
</div>
<div ng-if="policyName === 'hiddenImages'">
<img ng-src="{{offender | https}}" class="smallPreview checker"></img>
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'imagesTooLarge'">
<img ng-src="{{offender.url | https}}" class="smallPreview checker"></img>
<div>{{offender.width}}x{{offender.height}}</div>
<url-link url="offender.url" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'emptyRequests' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'cachingTooShort'">
<url-link url="offender.file" max-length="100"></url-link>
<url-link url="offender.url" max-length="100"></url-link>
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
</div>
@@ -151,14 +179,30 @@
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
</div>
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded' || policyName === 'synchronousXHR'">
<div ng-if="policyName === 'globalVariables'">
{{offender}}
</div>
<div ng-if="policyName === 'jQueryVersionsLoaded'">
{{offender.version}}
</div>
<div ng-if="policyName === 'synchronousXHR'">
{{offender.url}}
</div>
<div ng-if="policyName === 'fontsCount'">
<url-link url="offender.url" max-length="70"></url-link>
({{offender.size | bytes}})
</div>
<div ng-if="policyName === 'oldHttpProtocol'">
<b>{{offender.domain}}</b> sends <span ng-class="offender.requests > 4 ? 'offenderProblem' : ''"><b><ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize></b></span> over {{offender.httpVersion}}
</div>
<div ng-if="policyName === 'oldTlsProtocol'">
<b>{{offender.domain}}</b> uses {{offender.tlsVersion}} <span ng-if="offender.beforeDomReady === true" class="offenderProblem">and seems to be on the critical path</span>
</div>
</div>
</div>
</div>
@@ -167,8 +211,8 @@
<h3>
<ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
in
<url-link ng-if="fileDetails.url !== 'Inline CSS'" url="fileDetails.url" max-length="80"></url-link>
<span ng-if="fileDetails.url === 'Inline CSS'">inline CSS</span>
<url-link ng-if="fileDetails.url !== 'Inline CSS' && fileDetails.url !== '[inline CSS]'" url="fileDetails.url" max-length="80"></url-link>
<span ng-if="fileDetails.url === 'Inline CSS' || fileDetails.url === '[inline CSS]'">inline CSS</span>
</h3>
<div class="offendersTable">
@@ -198,11 +242,6 @@
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssExpressions'">
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssImportants'">
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
@@ -223,19 +262,6 @@
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div>
<div ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
</div>
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
</div>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette checker">
@@ -261,6 +287,36 @@
</div>
</div>
<div ng-if="policyName === 'DOMaccesses'">
<div ng-repeat="(type, list) in rule.offendersObj.list.byType">
<h3>
<ng-pluralize count="list.length" when="{'0': 'No offender', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize> from
<span ng-if="type === 'DOMqueriesById'">getElementById()</span>
<span ng-if="type === 'DOMqueriesByTagName'">getElementsByTagName()</span>
<span ng-if="type === 'DOMqueriesByClassName'">getElementsByClassName()</span>
<span ng-if="type === 'DOMqueriesByQuerySelectorAll'">querySelector() or querySelectorAll()</span>
<span ng-if="type === 'DOMinserts'">appendChild() or insertBefore()</span>
<span ng-if="type === 'DOMmutationsInserts'">added nodes</span>
<span ng-if="type === 'DOMmutationsRemoves'">removed nodes</span>
<span ng-if="type === 'DOMmutationsAttributes'">attribute changes</span>
<span ng-if="type === 'eventsBound'">addEventListener()</span>
</h3>
<div class="offendersTable">
<div ng-repeat="access in list">
<div ng-if="type === 'DOMqueriesById'">#{{access.id}}</div>
<div ng-if="type === 'DOMqueriesByTagName'">{{access.tag}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
<div ng-if="type === 'DOMqueriesByClassName'">.{{access.class}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
<div ng-if="type === 'DOMqueriesByQuerySelectorAll'">{{access.selector}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
<div ng-if="type === 'DOMinserts'"><span title="{{access.append}}">{{access.append | lastDOMNode}}</span> <b>added to</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
<div ng-if="type === 'DOMmutationsInserts'">{{access.node}} <b>added to</b> {{access.target}}</div>
<div ng-if="type === 'DOMmutationsRemoves'">{{access.node}} <b>removed from</b> {{access.target}}</div>
<div ng-if="type === 'DOMmutationsAttributes'">{{access.attribute}} <b>changed on</b> {{access.node}}</div>
<div ng-if="type === 'eventsBound'">{{access.eventType}} <b>on</b> <span title="{{access.path}}">{{access.path | lastDOMNode}}</span></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'imageOptimization'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
<div class="imageOffenders">
@@ -270,32 +326,40 @@
<div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url | https}}" class="checker" /></a></div>
</div>
<div>
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
<p ng-if="!image.isCompressible || image.isCompressed">Current weight: {{image.originalWeigth | bytes}}</p>
<p ng-if="image.isCompressible && !image.isCompressed">Current weight: {{image.originalWeigth | bytes}} ({{image.originalCompressedWeight | bytes}} compressed)</p>
<p ng-if="image.lossless && image.isCompressible">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} compressed (<b>-{{image.gain | bytes}}</b> compressed)</p>
<p ng-if="image.lossless && !image.isCompressible">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
<p ng-if="image.lossy && image.isCompressible">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} compressed (<b>-{{image.gain | bytes}} compressed</b>)</p>
<p ng-if="image.lossy && !image.isCompressible">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'gzipCompression'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div ng-if="policyName === 'compression'">
<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>Gzipped</div>
<div>Gzip weight</div>
<div>Brotli</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
<url-link url="file.url" max-length="60"></url-link>
</div>
<div>{{file.original | bytes}}</div>
<div>{{file.gzipped | bytes}}</div>
<div>{{file.originalSize | bytes}}</div>
<div ng-if="file.wasCompressed"><i>already gzipped</i></div>
<div ng-if="!file.wasCompressed">{{file.gzipped | bytes}}</div>
<div>{{file.brotlified | bytes}}</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
@@ -312,12 +376,12 @@
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
<url-link url="file.url" max-length="60"></url-link>
</div>
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
<div ng-if="file.isCompressed">{{file.originalWeigth | bytes}} (compressed)</div>
<div ng-if="!file.isCompressed">{{file.originalWeigth | bytes}} ({{file.originalCompressedWeight | bytes}} compressed)</div>
<div ng-if="file.isCompressed">{{file.afterOptimizationAndCompression | bytes}} (compressed)</div>
<div ng-if="!file.isCompressed">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} compressed)</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
@@ -351,51 +415,8 @@
</div>
</div>
<div ng-if="policyName === 'smallRequests'">
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
<div class="table">
<div class="headers">
<div ng-if="type == 'image'">Preview</div>
<div>File</div>
<div>Weight (bytes)</div>
</div>
<div ng-repeat="request in requests track by $index">
<div ng-if="type == 'image'"><img ng-src="{{request.url | https}}" class="smallPreview checker" /></div>
<div><url-link url="request.url" max-length="100"></url-link></div>
<div>{{request.size}}</div>
</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>
</div>
<div ng-if="policyName === 'queriesWithoutResults'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
</div>
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
</div>
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
<h3>{{rule.value}} offenders</h3>
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-colors="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
<div ng-if="rule.value > 0">
<h3>Breakpoints list</h3>
<div class="offendersTable">
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
@@ -464,14 +485,22 @@
</div>
</div>
<div ng-if="policyName === 'http2'">
<h3>Protocols advertised by the server</h3>
<div class="offendersTable">
<div ng-repeat="protocol in rule.offendersObj.list">
<div>{{protocol}}</div>
<div ng-if="policyName === 'nonWoff2Fonts'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.fonts.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>WOFF 2 weight</div>
<div>Gain</div>
</div>
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
<div>none</div>
<div ng-repeat="file in rule.offendersObj.list.fonts | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div>{{file.originalSize | bytes}}</div>
<div>{{file.woff2Size | bytes}}</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
+1 -3
View File
@@ -2,14 +2,12 @@
<div class="screenshot board">
<h2>Screenshot</h2>
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<div class="screenshotWrapper" ng-class="result.params.options.device || 'phone'">
<div>
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
</div>
</div>
<p>(scroll on the screenshot to see under the fold)</p>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
-100
View File
@@ -1,100 +0,0 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="execution board">
<div class="selectScript" ng-class="{empty:!selectedScript}">
Filter timeline and profiler by script:
<select ng-model="selectedScript" ng-options="script.shortPath for script in scripts" ng-change="changeScript()">
<option value="">All (no filter)</option>
</select>
</div>
<h2>JavaScript Timeline</h2>
<p>This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.</p>
<div class="timeline">
<div class="chart">
<div class="chartPoints">
<div ng-repeat="duration in timeline track by $index"
class="interval"
ng-class="{
domCreation: $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domInteractive,
domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
<div class="tooltip detailsOverlay">
<div>
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
</div>
</div>
</div>
</div>
<div class="startTime">0 ms</div>
<div class="endTime">{{endTime | number: 0}} ms</div>
</div>
<div class="legend">
<div class="titles">
<div class="domCreation"><div class="color"></div>DOM creation</div>
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
<div class="domComplete"><div class="color"></div>Page is complete</div>
</div>
<div class="tips">
<div>Executing JavaScript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
<div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
</div>
</div>
</div>
<h2>JavaScript Profiler</h2>
<p>
The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads.
</p>
<div class="filters">
<div>
<input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Filter on warnings and errors</label>
<div class="subFilters" ng-if="warningsFilterOn">
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
<label for="queryWithoutResultsFilterOn">Queries without results</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
<label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
<label for="eventNotDelegatedFilterOn">Events not delegated</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
<label for="jsErrorFilterOn">Errors</label>
</div>
</div>
</div>
{{queryWithoutResultsFilterOn}}
</div>
<div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
<div>Timestamp</div>
</div>
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
+4
View File
@@ -24,9 +24,13 @@ var yellowLabTools = function(url, options) {
};
var runner = new Runner(params)
.progress(deferred.notify)
.then(function(data) {
deferred.resolve(data);
})
.fail(function(err) {
deferred.reject(err);
});
+172 -345
View File
@@ -15,9 +15,9 @@ var policies = {
"tool": "phantomas",
"label": "DOM max depth",
"message": "<p>A deep DOM makes the CSS matching with DOM elements difficult.</p><p>It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.</p>",
"isOkThreshold": 12,
"isBadThreshold": 22,
"isAbnormalThreshold": 30,
"isOkThreshold": 15,
"isBadThreshold": 25,
"isAbnormalThreshold": 32,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var domArrays = offenders.map(offendersHelpers.domPathToArray);
@@ -32,89 +32,38 @@ var policies = {
"tool": "phantomas",
"label": "Number of iframes",
"message": "<p>iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.</p>",
"isOkThreshold": 3,
"isOkThreshold": 4,
"isBadThreshold": 15,
"isAbnormalThreshold": 30,
"hasOffenders": false
"hasOffenders": true
},
"DOMidDuplicated": {
"tool": "phantomas",
"label": "IDs duplicated",
"message": "<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>",
"isOkThreshold": 0,
"isBadThreshold": 5,
"isBadThreshold": 10,
"isAbnormalThreshold": 50,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^(.*): ?(\d+) ?occurrences$/.exec(offender);
if (!parts) {
debug('DOMidDuplicated offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
id: parts[1],
occurrences: parseInt(parts[2], 10)
};
})
};
}
"hasOffenders": true
},
"DOMaccesses": {
"tool": "jsExecutionTransformer",
"label": "DOM access",
"message": "<p>This metric counts the number of calls to DOM related functions (both native DOM functions and jQuery functions) on page load.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost. Try to use <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a> as much as possible.</p>",
"isOkThreshold": 200,
"scriptDuration": {
"tool": "phantomas",
"label": "Total JS execution time",
"message": "<p>This is the number of milliseconds spent by the browser on JavaScript execution during page load.</p><p>For more details, try using the performance tab in Chrome DevTools. It is a bit complicated at first sight, but you'll be able to analyze exactly where this execution time is spent.</p>",
"isOkThreshold": 500,
"isBadThreshold": 2000,
"isAbnormalThreshold": 4000,
"hasOffenders": false
"hasOffenders": false,
"unit": 'ms'
},
"queriesWithoutResults": {
"tool": "jsExecutionTransformer",
"label": "Queries without result",
"message": "<p>Number of queries that return no result. Both native and jQuery DOM requests are counted.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
"isOkThreshold": 0,
"isBadThreshold": 150,
"isAbnormalThreshold": 250,
"hasOffenders": false
},
"DOMqueriesAvoidable": {
"tool": "phantomas",
"label": "Duplicated DOM queries",
"message": "<p>This is the number of queries that could be avoided by removing all duplicated queries.</p><p>Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.</p>",
"isOkThreshold": 0,
"isBadThreshold": 300,
"isAbnormalThreshold": 600,
"hasOffenders": true,
"takeOffendersFrom": "DOMqueriesDuplicated",
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^[^"]* ?"(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender);
if (!parts) {
debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
query: parts[1],
context: offendersHelpers.domPathToDomElementObj(parts[3]),
fn: parts[2],
count: parseInt(parts[4], 10)
};
})
};
}
"DOMaccesses": {
"tool": "domAccessAgregator",
"label": "DOM access",
"message": "<p>This metric estimates the number of times the JavaScript reads, changes or binds the DOM.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost.</p>",
"isOkThreshold": 500,
"isBadThreshold": 2500,
"isAbnormalThreshold": 5000,
"hasOffenders": true
},
"eventsScrollBound": {
"tool": "phantomas",
@@ -147,25 +96,13 @@ var policies = {
};
}
},
"DOMaccessesOnScroll": {
"tool": "jsExecutionTransformer",
"label": "DOM access on scroll",
"message": "<p>This rule counts the number of DOM-accessing functions calls on a scroll event, such as queries, readings, writings, bindings and jQuery functions.</p><p>Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.</p><p>One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.</p><p>Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use <a href=\"http://blogorama.nerdworks.in/javascriptfunctionthrottlingan/\" target=\"_blank\">throttling or debouncing</a>.</p>",
"isOkThreshold": 1,
"isBadThreshold": 20,
"isAbnormalThreshold": 35,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"jsErrors": {
"tool": "phantomas",
"label": "JavaScript errors",
"message": "<p>Just to let you know there are some errors on the page.</p><p><b>Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.</b></p>",
"message": "<p>Just to let you know there are some errors on the page.</p>",
"isOkThreshold": 0,
"isBadThreshold": 1,
"isAbnormalThreshold": 4,
"isAbnormalThreshold": 5,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -195,8 +132,8 @@ var policies = {
"label": "document.write calls",
"message": "<p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p><p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href=\"https://github.com/krux/postscribe\" target=\"_blank\">PostScribe</a>.</p>",
"isOkThreshold": 0,
"isBadThreshold": 3,
"isAbnormalThreshold": 8,
"isBadThreshold": 2,
"isAbnormalThreshold": 6,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -250,22 +187,13 @@ var policies = {
"isAbnormalThreshold": 1,
"hasOffenders": true
},
"consoleMessages": {
"tool": "phantomas",
"label": "Console messages",
"message": "<p>Try to keep your console clean when in production. Debugging is good for development only.</p><p>Writing in the console has a cost, especially when dumping large object variables.</p><p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>",
"isOkThreshold": 3,
"isBadThreshold": 20,
"isAbnormalThreshold": 50,
"hasOffenders": false
},
"globalVariables": {
"tool": "phantomas",
"label": "Global variables",
"message": "<p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p><p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>",
"isOkThreshold": 40,
"isBadThreshold": 200,
"isAbnormalThreshold": 700,
"isOkThreshold": 20,
"isBadThreshold": 300,
"isAbnormalThreshold": 800,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -276,7 +204,7 @@ var policies = {
},
"jQueryVersion": {
"label": "jQuery version",
"message": "<p>The current latest version of jQuery is 3.3</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
"message": "<p>The current latest version of jQuery is 3.5</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
"hasOffenders": false,
"scoreFn": function(data) {
var differentVersions = data.toolsResults.phantomas.metrics.jQueryVersionsLoaded;
@@ -291,6 +219,8 @@ var policies = {
if (value.indexOf('3.3.') === 0 ||
value.indexOf('3.4.') === 0 ||
value.indexOf('3.5.') === 0 ||
value.indexOf('3.6.') === 0 ||
value.indexOf('3.7.') === 0 ||
value.indexOf('4.0.') === 0) {
score = 100;
} else if (value.indexOf('3.2.') === 0) {
@@ -345,76 +275,45 @@ var policies = {
"message": "<p>jQuery is a heavy library. You should <b>never</b> load jQuery more than once on the same page.</p>",
"isOkThreshold": 1,
"isBadThreshold": 2,
"isAbnormalThreshold": 2,
"isAbnormalThreshold": 3,
"hasOffenders": true
},
"jQueryCallsOnEmptyObject": {
"tool": "jsExecutionTransformer",
"label": "Calls on empty objects",
"message": "<p>This metric counts the number of jQuery functions called on an empty jQuery object. The call was useless.</p><p>This can be helpful to detect dead or unused code.</p>",
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": false
},
"jQueryNotDelegatedEvents": {
"tool": "jsExecutionTransformer",
"label": "Events not delegated",
"message": "<p>This is the number of events that are bound with the .bind() or the .on() function without using <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a>.</p><p>This means jQuery binds each element contained in the object one by one. This is bad for performance.</p>",
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": false
},
"cssParsingErrors": {
"tool": "phantomas",
"label": "CSS syntax error",
"message": "<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p><p>Maybe a <a href=\"http://jigsaw.w3.org/css-validator\" target=\"_blank\">CSS validator</a> can help you.</p>",
"isOkThreshold": 0,
"isBadThreshold": 1,
"isBadThreshold": 2,
"isAbnormalThreshold": 20,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
if (offender === '[inline CSS] (Empty CSS was provided)') {
if (offender === '[inline CSS]') {
return {
error: 'Empty style tag',
error: 'Empty <style> tag',
file: null,
line: null,
column: null
};
}
var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender);
if (parts) {
if (offender.value) {
return {
error: parts[2] || 'Unknown parsing error' + (parts[1] ? '. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.' : ''),
file: parts[1] || null,
line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null,
column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null
error: offender.value.message || 'Unknown parsing error. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.',
file: offender.url || null,
line: offender.value.position ? offender.value.position.start.line : null,
column: offender.value.position ? offender.value.position.start.column : null
};
} else {
return {
error: 'Unknown parsing error. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.',
file: offender,
line: null,
column: null
};
}
// Try another syntax
parts = /^(.*) <(.*)> @ (\d+):(\d+)$/.exec(offender);
if (parts) {
return {
error: parts[1] || 'Unknown parsing error',
file: parts[2] || null,
line: parseInt(parts[3], 10),
column: parseInt(parts[4], 10)
};
}
debug('cssParsingErrors offenders transform function error with "%s"', offender);
return {
parseError: offender
};
})
};
}
@@ -423,51 +322,18 @@ var policies = {
"tool": "phantomas",
"label": "Rules count",
"message": "<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p><p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom file per page.</p>",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 4500,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var hasInline = false;
var inlineCount = 0;
var files = [];
offenders.forEach(function(line) {
if (line.indexOf('[inline CSS]: ') === 0) {
hasInline = true;
inlineCount += parseInt(line.substr(14));
} else {
var parts = /^<(.*)>: (\d+)$/.exec(line);
if (parts) {
files.push({
file: parts[1],
rules: parseInt(parts[2], 10)
});
}
}
});
if (hasInline) {
files.push({
file: 'inline CSS',
rules: inlineCount
});
}
return {
count: files.length,
list: files
};
}
"isOkThreshold": 2000,
"isBadThreshold": 5000,
"isAbnormalThreshold": 8000,
"hasOffenders": true
},
"cssComplexSelectors": {
"tool": "phantomas",
"label": "Complex selectors",
"message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors. The <a href=\"http://getbem.com\" target=\"_blank\">B.E.M. methodology</a> is an useful way to simplify your CSS.</p>",
"message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors. The <a href=\"http://getbem.com\" target=\"_blank\">B.E.M. methodology</a> is a useful way to simplify your CSS.</p>",
"isOkThreshold": 0,
"isBadThreshold": 800,
"isAbnormalThreshold": 2000,
"isBadThreshold": 1000,
"isAbnormalThreshold": 24000,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -482,18 +348,18 @@ var policies = {
"tool": "phantomas",
"label": "Colors count",
"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": 50,
"isBadThreshold": 150,
"isAbnormalThreshold": 400,
"isOkThreshold": 100,
"isBadThreshold": 200,
"isAbnormalThreshold": 500,
"hasOffenders": true,
"offendersTransformFn": function(offenders, ruleObject) {
var deduplicatedObj = {};
offenders.map(function(offender) {
var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender);
var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender.value.message);
if (!parts) {
debug('cssColors offenders transform function error with "%s"', offender);
debug('cssColors offenders transform function error with "%s"', offender.value.message);
return;
}
@@ -529,8 +395,8 @@ var policies = {
"label": "Similar colors",
"message": "<p>This is the list of colors found in the stylesheets, that are very close to each other. The eye can barely see the difference.</p><p>Use this list to reduce the number of colors in your palette, it will be easier to maintain.</p>",
"isOkThreshold": 0,
"isBadThreshold": 40,
"isAbnormalThreshold": 80,
"isBadThreshold": 60,
"isAbnormalThreshold": 120,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -543,9 +409,9 @@ var policies = {
"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,
"isOkThreshold": 10,
"isBadThreshold": 50,
"isAbnormalThreshold": 80,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var offendersTable = [];
@@ -565,9 +431,9 @@ var policies = {
"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": 50,
"isBadThreshold": 250,
"isAbnormalThreshold": 1000,
"isOkThreshold": 60,
"isBadThreshold": 400,
"isAbnormalThreshold": 1200,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offendersHelpers.orderByFile(offenders);
@@ -596,12 +462,12 @@ var policies = {
"label": "Duplicated selectors",
"message": "<p>This is when two or more selectors are strictly identical and should be merged.</p>",
"isOkThreshold": 0,
"isBadThreshold": 50,
"isAbnormalThreshold": 100,
"isBadThreshold": 100,
"isAbnormalThreshold": 200,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
var parts = /^(.*) \((\d+) times\) ?<(.*)>$/.exec(offender);
var parts = /^(.*) \((\d+) times\)$/.exec(offender.value.message);
if (!parts) {
debug('cssDuplicatedSelectors offenders transform function error with "%s"', offender);
@@ -613,7 +479,7 @@ var policies = {
return {
rule: parts[1],
occurrences: parseInt(parts[2], 10),
file: parts[3]
file: offender.value.url
};
});
@@ -625,8 +491,8 @@ var policies = {
"label": "Duplicated properties",
"message": "<p>This is the number of property definitions duplicated within a selector.</p>",
"isOkThreshold": 0,
"isBadThreshold": 60,
"isAbnormalThreshold": 120,
"isBadThreshold": 100,
"isAbnormalThreshold": 200,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -659,54 +525,20 @@ var policies = {
"message": "<p>Very easy to fix: remove all empty rules.</p>",
"isOkThreshold": 0,
"isBadThreshold": 50,
"isAbnormalThreshold": 100,
"isAbnormalThreshold": 120,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(offendersHelpers.cssOffenderPattern);
return offendersHelpers.orderByFile(parsedOffenders);
}
},
"cssExpressions": {
"tool": "phantomas",
"label": "CSS expressions",
"message": "<p>Such as: expression( document.body.clientWidth > 600 ? \"600px\" : \"auto\" )</p><p>This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.</p>",
"isOkThreshold": 0,
"isBadThreshold": 1,
"isAbnormalThreshold": 20,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
var parts = /^(.*) {([^ ]+): expression\((.*)\)}$/.exec(splittedOffender.css);
if (!parts) {
debug('cssExpressions offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
rule: parts[1],
property: parts[2],
expression: parts[3],
file: splittedOffender.file,
line: splittedOffender.line,
column: splittedOffender.column
};
});
return offendersHelpers.orderByFile(parsedOffenders);
}
},
"cssImportants": {
"tool": "phantomas",
"label": "Uses of !important",
"message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>",
"isOkThreshold": 0,
"isBadThreshold": 75,
"isAbnormalThreshold": 200,
"isBadThreshold": 200,
"isAbnormalThreshold": 500,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -739,8 +571,8 @@ var policies = {
"label": "Old IE fixes",
"message": "<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p><p>IE6:<ul><li>* html</li><li>html > body (everything but IE6)</li></ul><p><p>IE7:<ul><li><b>*</b>height: 123px;</li><li>height: 123px <b>!ie</b>;</li></ul><p><p>IE9:<ul><li>-ms-filter</li><li>progid:DXImageTransform.Microsoft</li></ul></p>",
"isOkThreshold": 0,
"isBadThreshold": 50,
"isAbnormalThreshold": 150,
"isBadThreshold": 100,
"isAbnormalThreshold": 300,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -795,8 +627,8 @@ var policies = {
"label": "Old prefixes",
"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": 50,
"isAbnormalThreshold": 200,
"isBadThreshold": 100,
"isAbnormalThreshold": 400,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var properties = {};
@@ -848,8 +680,8 @@ var policies = {
"label": "Redundant body selectors",
"message": "<p>This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>",
"isOkThreshold": 0,
"isBadThreshold": 60,
"isAbnormalThreshold": 200,
"isBadThreshold": 120,
"isAbnormalThreshold": 400,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -866,8 +698,8 @@ var policies = {
"label": "Redundant tags selectors",
"message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" tag is nearly always inside an \"ul\" container (the \"ol\" container is quite rare). Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
"isOkThreshold": 0,
"isBadThreshold": 60,
"isAbnormalThreshold": 200,
"isBadThreshold": 120,
"isAbnormalThreshold": 400,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -897,37 +729,46 @@ var policies = {
"totalWeight": {
"tool": "redownload",
"label": "Total weight",
"message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
"isOkThreshold": 1048576,
"isBadThreshold": 2621440,
"isAbnormalThreshold": 4194304,
"message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.</p>",
"isOkThreshold": 1572864,
"isBadThreshold": 3145728,
"isAbnormalThreshold": 5242880,
"hasOffenders": true,
"unit": 'bytes'
},
"imageOptimization": {
"tool": "redownload",
"label": "Image optimization",
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a>.</p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p>",
"isOkThreshold": 20480,
"isBadThreshold": 204800,
"isAbnormalThreshold": 307200,
"hasOffenders": true,
"unit": 'bytes'
},
"gzipCompression": {
"imagesTooLarge": {
"tool": "redownload",
"label": "Gzip compression",
"message": "<p>Measures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
"isOkThreshold": 5125,
"isBadThreshold": 81920,
"isAbnormalThreshold": 153600,
"label": "Oversized images",
"message": "<p>This is the number of images with a width >1200px on mobile, >1800px on tablet, >2400 on desktop, >3200px on HD desktop. Try reducing their size.</p><p>Please ignore if the file is used as a sprite.</p>",
"isOkThreshold": 0,
"isBadThreshold": 5,
"isAbnormalThreshold": 10,
"hasOffenders": true
},
"compression": {
"tool": "redownload",
"label": "Gzip/Brotli compression",
"message": "<p>Measures the number of bytes that could be saved by compressing textual files. Some files listed below might not be compressed at all, some might be already compressed with Gzip but would become even lighter with Brotli.</p><p>All major server systems are now compatible with Brotli.</p><p>Note that compressing small files (< 1 KB) is arguable, and that some assets such as images should not be compressed as it is already included in their format. <a href=\"https://letstalkaboutwebperf.com/en/gzip-brotli-server-config/\" target=\"_blank\">Here</a> is a list of MIME types that should be compressed.</p>",
"isOkThreshold": 20480,
"isBadThreshold": 204800,
"isAbnormalThreshold": 409600,
"hasOffenders": true,
"unit": 'bytes'
},
"fileMinification": {
"tool": "redownload",
"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>. These tools are so good that some of your minified files can be marked as unminified. Change your tool it this happens :)</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>",
"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>. These tools are so good that some of your minified files can be marked as unminified. Change your tool if this happens :)</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": 5125,
"isBadThreshold": 61440,
"isAbnormalThreshold": 122880,
@@ -937,40 +778,20 @@ var policies = {
"totalRequests": {
"tool": "redownload",
"label": "Requests number",
"message": "<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
"isOkThreshold": 20,
"isBadThreshold": 120,
"isAbnormalThreshold": 200,
"message": "<p>Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
"isOkThreshold": 80,
"isBadThreshold": 240,
"isAbnormalThreshold": 320,
"hasOffenders": true
},
"domains": {
"tool": "phantomas",
"label": "Different domains",
"message": "<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p><p>By the way, domain sharding is not a good practice anymore.</p>",
"isOkThreshold": 10,
"isBadThreshold": 25,
"isAbnormalThreshold": 50,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender);
if (!parts) {
debug('domains offenders transform function error with "%s"', offender);
return {
file: offender
};
}
return {
domain: parts[1],
requests: parseInt(parts[2])
};
})
};
}
"isOkThreshold": 12,
"isBadThreshold": 30,
"isAbnormalThreshold": 60,
"hasOffenders": true
},
"notFound": {
"tool": "phantomas",
@@ -1011,19 +832,10 @@ var policies = {
"isAbnormalThreshold": 5,
"hasOffenders": true
},
"smallRequests": {
"tool": "redownload",
"label": "Small requests",
"message": "<p>List of all requests that are less than 2 KB. Try to merge them with other files.</p>",
"isOkThreshold": 10,
"isBadThreshold": 50,
"isAbnormalThreshold": 80,
"hasOffenders": true
},
"lazyLoadableImagesBelowTheFold": {
"tool": "phantomas",
"label": "Below the fold images",
"message": "<p>This is the number of images displayed below the fold that could be lazy-loaded. This is an excellent way to accelerate the loading time of an heavy page.</p><p>I recommend using <a href=\"https://github.com/vvo/lazyload\" target=\"_blank\">this lazyloader</a>.</p>",
"message": "<p>This is the number of images displayed below the fold that could be lazy-loaded. This is an excellent way to accelerate the loading time of an heavy page.</p><p>I recommend using <a href=\"https://github.com/aFarkas/lazysizes\" target=\"_blank\">lazysizes</a> or <a href=\"https://github.com/ApoorvSaxena/lozad.js\" target=\"_blank\">Lozad</a>.</p>",
"isOkThreshold": 1,
"isBadThreshold": 12,
"isAbnormalThreshold": 30,
@@ -1044,7 +856,7 @@ var policies = {
"message": "<p>This is the number of custom web fonts loaded on the page.</p><p>Webfonts are beautiful, but heavy. You should keep their number as low as possible.</p>",
"isOkThreshold": 1,
"isBadThreshold": 5,
"isAbnormalThreshold": 7,
"isAbnormalThreshold": 8,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
@@ -1053,7 +865,7 @@ var policies = {
"heavyFonts": {
"tool": "redownload",
"label": "Overweighted webfonts",
"message": "<p>This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.</p><p>It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.</p><p>Sorry, Yellow Lab Tools is not yet compatible with the WOFF2 font format that generates 20-30% smaller fonts. You can proceed to a manual verification on a modern browser.</p>",
"message": "<p>This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.</p><p>It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.</p>",
"isOkThreshold": 0,
"isBadThreshold": 102400,
"isAbnormalThreshold": 204800,
@@ -1063,7 +875,7 @@ var policies = {
return offenders;
}
},
"unusedUnicodeRanges": {
/*"unusedUnicodeRanges": {
"tool": "redownload",
"label": "Unused Unicode ranges",
"message": "<p>This metric counts the number of unused Unicode ranges inside each font. For example, one font could include Cyrillic glyphs but none of them are used on the page.</p><p>It also reveals the number of ligatures (letters that are represented differently when close to each other) and hidden chars (glyphs not linked to the unicode system that can't be displayed on the web).</p><p>Because of technical limitations, Yellow Lab Tools checks each font against the glyphs of the entire page. As a result, estimated use is >= to reality. For example, if you read that 10 glyphs are \"possibly used\", it means that these 10 glyphs are used on the page but nothing guaranties that they are displayed using this font.</p><p>Tools such as <a href=\"https://www.fontsquirrel.com/tools/webfont-generator\" target=\"_blank\">Font Squirrel</a> can remove some unicode ranges from a font.</p><p>In the case of an icon font, make sure you only keep the icons that are used on the website and to remove the others. Several tools are able to extract SVG images from a font, then some other tools can generate a font from the SVGs you want to keep.</p>",
@@ -1074,43 +886,73 @@ var policies = {
"offendersTransformFn": function(offenders) {
return offenders;
}
},*/
"nonWoff2Fonts": {
"tool": "redownload",
"label": "WOFF 2",
"message": "<p>The fonts listed here could be lighter if they were served with the latest WOFF 2 font file format. Some online tools can help you easily convert older formats to WOFF 2.</p>",
"isOkThreshold": 0,
"isBadThreshold": 51200,
"isAbnormalThreshold": 122880,
"hasOffenders": true,
"unit": 'bytes'
},
"http2": {
"label": "HTTP/2 or SPDY",
"message": "<p>HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.</p><p>The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.</p>",
"oldHttpProtocol": {
"label": "HTTP protocols",
"message": "<p>HTTP/2 is the latest version of the HTTP protocol. It is designed to optimize load speed. HTTP/3 will come soon and should be even faster!</p><p>When a domain sends more than 4 requests over HTTP/1, this metric counts one point for each new request. Below 5 requests, the benefits of HTTP/2 are generally less significant.</p>",
"hasOffenders": true,
"scoreFn": function(data) {
if (!data.toolsResults.http2) {
return null;
var count = 0;
var offenders = data.toolsResults.phantomas.offenders.oldHttpProtocol;
if (offenders) {
offenders.forEach(function(offender) {
if (offender.requests > 4) {
count += offender.requests - 4;
}
});
}
var isHttp2 = data.toolsResults.http2.metrics.http2;
var isOkThreshold = 0;
var isBadThreshold = 25;
var isAbnormalThreshold = 100;
var score = (isBadThreshold - count) * 100 / (isBadThreshold - isOkThreshold);
var abnormalityScore = (isAbnormalThreshold - count) * 100 / (isAbnormalThreshold - isOkThreshold);
var result = {
value: isHttp2 ? 'Yes' : 'No',
score: isHttp2 ? 100 : 0,
bad: !isHttp2,
abnormal: false,
abnormalityScore: 0
value: count,
score: Math.min(Math.max(Math.round(score), 0), 100),
bad: count > isBadThreshold,
abnormal: count > isAbnormalThreshold,
abnormalityScore: Math.min(Math.round(abnormalityScore), 0),
offendersObj: {
count: data.toolsResults.phantomas.metrics.oldHttpProtocol,
list: data.toolsResults.phantomas.offenders.oldHttpProtocol || []
}
};
if (data.toolsResults.http2.offenders) {
result.offendersObj = {
count: data.toolsResults.http2.offenders.http2.length,
list: data.toolsResults.http2.offenders.http2
};
}
return result;
}
},
"oldTlsProtocol": {
"tool": "phantomas",
"label": "TLS protocols",
"message": "<p>Counts the number of domains that use TLS versions < 1.3. This is the latest version and it includes a faster \"handshake\" technology.</p><p>The 1.0 and 1.1 versions are deprecated because they are unsafe, switch to 1.2 or above as soon as possible.</p><p>The version 1.3 includes an even faster option called 0-RTT, check your server compatibility on <a href='https://www.ssllabs.com/ssltest/' target=\"_blank\">SSL Labs</a>.</p>",
"isOkThreshold": 0,
"isBadThreshold": 5,
"isAbnormalThreshold": 15,
"hasOffenders": true
},
"cachingDisabled": {
"tool": "phantomas",
"label": "Caching disabled",
"message": "<p>Counts responses with caching disabled (max-age=0)</p><p>Fix immediatly if on static assets.</p>",
"isOkThreshold": 0,
"isBadThreshold": 12,
"isAbnormalThreshold": 25,
"isOkThreshold": 2,
"isBadThreshold": 15,
"isAbnormalThreshold": 30,
"hasOffenders": true
},
"cachingNotSpecified": {
@@ -1127,29 +969,14 @@ var policies = {
"label": "Caching too short",
"message": "<p>Responses with too short caching time (less than a week).</p><p>The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.</p>",
"isOkThreshold": 5,
"isBadThreshold": 20,
"isAbnormalThreshold": 40,
"isBadThreshold": 25,
"isAbnormalThreshold": 50,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders
.map(function(offender) {
var parts = /^([^ ]*) cached for (-?\d+(\.\d+)?) s$/.exec(offender);
if (!parts) {
debug('cachingTooShort offenders transform function error with "%s"', offender);
return {
file: offender
};
}
return {
file: parts[1],
ttl: Math.round(parseFloat(parts[2]))
};
}).sort(function(a, b) {
.sort(function(a, b) {
return a.ttl - b.ttl;
+23 -35
View File
@@ -5,19 +5,19 @@
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"gzipCompression": 2,
"fileMinification": 1
"imagesTooLarge": 1,
"compression": 2,
"fileMinification": 2
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 5,
"totalRequests": 2,
"domains": 3,
"notFound": 3,
"notFound": 2,
"identicalFiles": 2,
"emptyRequests": 3,
"smallRequests": 1,
"lazyLoadableImagesBelowTheFold": 2,
"hiddenImages": 1
}
@@ -31,28 +31,20 @@
"DOMidDuplicated": 1
}
},
"domManipulations": {
"label": "DOM manipulations",
"javascriptComplexity": {
"label": "JS complexity",
"policies": {
"DOMaccesses": 3,
"queriesWithoutResults": 1,
"DOMqueriesAvoidable": 1
}
},
"scroll": {
"label": "Scroll bottlenecks",
"policies": {
"eventsScrollBound": 1,
"DOMaccessesOnScroll": 4
"scriptDuration": 4,
"DOMaccesses": 2,
"eventsScrollBound": 1
}
},
"badJavascript": {
"label": "Bad JavaScript",
"label": "Bad JS",
"policies": {
"jsErrors": 1,
"documentWriteCalls": 2,
"synchronousXHR": 5,
"consoleMessages": 0.5,
"globalVariables": 0.5
}
},
@@ -60,9 +52,7 @@
"label": "jQuery",
"policies": {
"jQueryVersion": 2,
"jQueryVersionsLoaded": 2,
"jQueryCallsOnEmptyObject": 1,
"jQueryNotDelegatedEvents": 1
"jQueryVersionsLoaded": 2
}
},
"cssComplexity": {
@@ -79,13 +69,12 @@
"badCSS": {
"label": "Bad CSS",
"policies": {
"cssParsingErrors": 4,
"cssImports": 3,
"cssParsingErrors": 3,
"cssImports": 4,
"cssDuplicatedSelectors": 2,
"cssDuplicatedProperties": 1,
"cssEmptyRules": 2,
"cssExpressions": 1,
"cssImportants": 3,
"cssImportants": 2,
"cssOldIEFixes": 1,
"cssOldPropertyPrefixes": 1,
"cssRedundantBodySelectors": 1,
@@ -96,14 +85,15 @@
"label": "Web fonts",
"policies": {
"fontsCount": 1,
"heavyFonts": 1,
"unusedUnicodeRanges": 1
"heavyFonts": 0.5,
"nonWoff2Fonts": 0.5
}
},
"serverConfig": {
"label": "Server config",
"policies": {
"http2": 2,
"oldHttpProtocol": 2,
"oldTlsProtocol": 2,
"closedConnections": 2,
"cachingNotSpecified": 1,
"cachingDisabled": 1,
@@ -113,14 +103,12 @@
},
"globalScore": {
"pageWeight": 3,
"requests": 3,
"requests": 2,
"domComplexity": 2,
"domManipulations": 2,
"scroll": 1,
"javascriptComplexity": 2,
"badJavascript": 2,
"jQuery": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"jQuery": 0.5,
"cssComplexity": 0.5,
"badCSS": 1,
"fonts": 1,
"serverConfig": 1
+10 -37
View File
@@ -117,7 +117,7 @@ var OffendersHelpers = function() {
for (var i=0 ; i<traceArray.length ; i++) {
// Handle the new PhantomJS 2.x syntax
parts = /^(([\w$]+)@)?([^ ]+):(\d+):(\d+)$/.exec(traceArray[i]);
parts = /^\s*at( (\w+))? \(?([^ ]+):(\d+):(\d+)\)?$$/.exec(traceArray[i]);
if (parts) {
obj = {
@@ -132,25 +132,6 @@ var OffendersHelpers = function() {
results.push(obj);
} else {
// Old syntax
parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
if (parts) {
obj = {
file: parts[3],
line: parseInt(parts[4], 10)
};
if (parts[2]) {
obj.functionName = parts[2];
}
results.push(obj);
} else {
return null;
}
}
}
return results;
@@ -172,23 +153,15 @@ var OffendersHelpers = function() {
};
this.cssOffenderPattern = function(offender) {
// Remove any line breaks
offender = offender.replace(/(\r\n|\n|\r)/gm, '');
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) ?@ ?(\d+):(\d+)$/.exec(offender);
if (!parts) {
return {
offender: offender
};
} else {
return {
css: parts[1],
file: parts[2] || null,
line: parseInt(parts[3], 10),
column: parseInt(parts[4], 10)
};
}
// Used to work with strings
// As of Phantomas v2, offender is now in JSON format.
// Let's just adapt this for now and we'll see later if we remove completely this function
return {
css: offender.value.message,
file: offender.url,
line: offender.value.position.start.line,
column: offender.value.position.start.column
};
};
this.fileWithSizePattern = function(fileWithSize) {
+2 -1
View File
@@ -111,7 +111,8 @@ var RulesChecker = function() {
if (rule) {
rule.policy = {
label: policy.label,
message: policy.message
message: policy.message,
hasOffenders: policy.hasOffenders || false
};
results[metricName] = rule;
+60 -15
View File
@@ -2,10 +2,9 @@ var Q = require('q');
var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var colorDiff = require('./tools/colorDiff');
var domAccessAgregator = require('./tools/domAccessAgregator');
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
var isHttp2 = require('./tools/isHttp2');
var redownload = require('./tools/redownload/redownload');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -25,11 +24,33 @@ var Runner = function(params) {
// Execute Phantomas first
phantomasWrapper.execute(data)
// For the progress bar
.progress(function(event) {
if (event === 'domReady') {
deferred.notify({
estimatedProgress: 0.15,
milestone: 'domReady'
});
}
if (event === 'domComplete') {
deferred.notify({
estimatedProgress: 0.25,
milestone: 'domComplete'
});
}
})
.then(function(phantomasResults) {
// For the progress bar
deferred.notify({
estimatedProgress: 0.4,
milestone: 'phantomas'
});
data.toolsResults.phantomas = phantomasResults;
// Treat the JS Execution Tree from offenders
data = jsExecutionTransformer.transform(data);
// Mix all DOM Access metrics together
data = domAccessAgregator.agregate(data);
// Compare colors
data = colorDiff.compareAllColors(data);
@@ -38,16 +59,24 @@ var Runner = function(params) {
data = mediaQueriesChecker.analyzeMediaQueries(data);
// Redownload every file
return redownload.recheckAllFiles(data);
return redownload.recheckAllFiles(data)
.progress(function(redownloadedProgress) {
deferred.notify({
estimatedProgress: 0.4 + redownloadedProgress * 0.55,
milestone: 'phantomas'
});
});
})
.then(function(data) {
// Check if HTTP2
return isHttp2.check(data);
})
.then(function(data) {
// For the progress bar
deferred.notify({
estimatedProgress: 0.99,
milestone: 'redownload'
});
// Rules checker
var policies = require('./metadata/policies');
@@ -57,20 +86,36 @@ var Runner = function(params) {
// Scores calculator
var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
data.scoreProfiles = {
generic : scoreCalculator.calculate(data, scoreProfileGeneric)
generic : scoreCalculator.calculate(data, scoreProfileGeneric, true)
};
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
// Calculate "If you fix this issue, your new score is..." on each rule
debug('Calculate "If you fix this issue..." scores');
Object.keys(data.rules).forEach(function(ruleName) {
// Save current values
var oldScore = data.rules[ruleName].score;
var oldAbnormalityScore = data.rules[ruleName].abnormalityScore;
// Simulate a 100/100 score on a specific rule
data.rules[ruleName].score = 100;
data.rules[ruleName].abnormalityScore = 0;
// Calculate new score
data.rules[ruleName].globalScoreIfFixed = scoreCalculator.calculate(data, scoreProfileGeneric, false).globalScore;
// Revert values
data.rules[ruleName].score = oldScore;
data.rules[ruleName].abnormalityScore = oldAbnormalityScore;
});
debug('Calculating is finished');
if (data.toolsResults.phantomas.offenders.blockedRequests) {
data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
}
// Report WordPress detection
if (data.toolsResults.redownload.metrics.isWordPress === true) {
data.frameworks = {isWordPress: true};
}
// Finished!
deferred.resolve(data);
+9 -5
View File
@@ -4,7 +4,7 @@ var debug = require('debug')('ylt:scoreCalculator');
var ScoreCalculator = function() {
'use strict';
this.calculate = function(data, profile) {
this.calculate = function(data, profile, debugFlag) {
var results = {
categories: {}
@@ -13,7 +13,9 @@ var ScoreCalculator = function() {
var categoryName;
var weight;
debug('Starting calculating scores');
if (debugFlag) {
debug('Starting calculating scores');
}
// Calculate categories
for (categoryName in profile.categories) {
@@ -31,7 +33,7 @@ var ScoreCalculator = function() {
if (data.rules[policyName]) {
policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
categoryScore.push(policyScore, weight);
} else {
} else if (debugFlag) {
debug('Warning: could not find rule %s', policyName);
}
@@ -59,8 +61,10 @@ var ScoreCalculator = function() {
results.globalScore = Math.round(globalScore.getScore());
debug('Score calculation finished:');
debug(results);
if (debugFlag) {
debug('Score calculation finished:');
debug(results);
}
return results;
};
+54 -83
View File
@@ -1,6 +1,5 @@
var debug = require('debug')('ylt:screenshotHandler');
var Jimp = require('jimp');
var tmp = require('temporary');
var Q = require('q');
var fs = require('fs');
var path = require('path');
@@ -8,48 +7,24 @@ var path = require('path');
var screenshotHandler = function() {
this.getScreenshotTempFile = function() {
var screenshotTmpFolder = new tmp.Dir();
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.png');
var that = this;
return {
getTmpFolder: function() {
return screenshotTmpFolder;
},
getTmpFilePath: function() {
return tmpFilePath;
},
toThumbnail: function(width) {
return that.optimize(tmpFilePath, width);
},
deleteTmpFile: function() {
return that.deleteTmpFileAndFolder(tmpFilePath, screenshotTmpFolder);
}
};
};
var tmpFolderPath = 'tmp';
var tmpFolderFullPath = path.join(__dirname, '..', tmpFolderPath);
var tmpFileName = 'temp-screenshot.png';
var tmpFileFullPath = path.join(tmpFolderFullPath, tmpFileName);
this.optimize = function(imagePath, width) {
this.findAndOptimizeScreenshot = function(width) {
var that = this;
debug('Starting screenshot transformation');
return this.openImage(imagePath)
return this.openImage(tmpFileFullPath)
.then(function(image) {
that.deleteTmpFile(tmpFileFullPath);
return that.resizeImage(image, width);
})
.then(this.addWhiteBackground)
.then(this.toBuffer);
};
@@ -77,61 +52,33 @@ var screenshotHandler = function() {
var deferred = Q.defer();
var currentWidth = image.bitmap.width;
var ratio = newWidth / currentWidth;
image.scale(ratio, function(err, image){
if (err) {
debug('Could not resize image');
debug(err);
if (currentWidth > 0) {
var ratio = newWidth / currentWidth;
deferred.reject(err);
} else {
debug('Image correctly resized');
deferred.resolve(image);
}
});
image.scale(ratio, function(err, image){
if (err) {
debug('Could not resize image');
debug(err);
deferred.reject(err);
} else {
debug('Image correctly resized');
deferred.resolve(image);
}
});
} else {
deferred.reject('Could not resize an empty image');
}
return deferred.promise;
};
// If the page doesn't set a bg color, the default PhantomJS one is transparent
// When transforming PNG to JPG, transparent pixels become black.
// This is why we need to add a transparent background.
this.addWhiteBackground = function(image) {
var deferred = Q.defer();
// Create a canvas with the same dimensions as your image:
new Jimp(image.bitmap.width, image.bitmap.height, 0xFFFFFF, function(err, canvas){
if (err) {
debug('Could not create a white canvas');
debug(err);
deferred.reject(err);
} else {
// Paste original image on top of the canvas
canvas.composite(image, 0, 0, function(err, image){
if (err) {
debug('Could not paste image on the white canvas');
debug(err);
deferred.reject(err);
} else {
// Now image has a white background...
debug('White background correctly added');
deferred.resolve(image);
}
});
}
});
return deferred.promise;
};
this.toBuffer = function(image) {
var deferred = Q.defer();
image.quality(90).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
if (err) {
debug('Could not save image to buffer');
debug(err);
@@ -147,24 +94,48 @@ var screenshotHandler = function() {
};
this.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) {
this.deleteTmpFile = function(tmpFilePath) {
var deferred = Q.defer();
fs.unlink(tmpFilePath, function (err) {
if (err) {
debug('Screenshot file not found, could not be deleted. But it is not a problem.');
debug('Screenshot temporary file not found, could not be deleted. But it is not a problem.');
} else {
debug('Screenshot file deleted.');
debug('Screenshot temporary file deleted.');
}
screenshotTmpFolder.rmdir();
debug('Screenshot temp folder deleted');
deferred.resolve();
});
return deferred.promise;
};
// Create a /tmp folder on the project's root directory
this.createTmpScreenshotFolder = function() {
var deferred = Q.defer();
// Create the folder if it doesn't exist
fs.exists(tmpFolderFullPath, function(exists) {
if (exists) {
deferred.resolve();
} else {
debug('Creating the tmp image folder', tmpFolderFullPath);
fs.mkdir(tmpFolderFullPath, function(err) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
});
return deferred.promise;
};
this.getTmpFileRelativePath = function() {
return tmpFolderPath + '/' + tmpFileName;
};
};
module.exports = new screenshotHandler();
+26 -27
View File
@@ -51,11 +51,8 @@ var ApiController = function(app) {
}
};
// Create a temporary folder to save the screenshot
var screenshot;
if (run.params.screenshot) {
screenshot = ScreenshotHandler.getScreenshotTempFile();
}
// Create the tmp folder if it doesn't exist
ScreenshotHandler.createTmpScreenshotFolder(run.runId);
// Add test to the testQueue
debug('Adding test %s to the queue', run.runId);
@@ -78,7 +75,7 @@ var ApiController = function(app) {
console.log('Launching test ' + run.runId + ' on ' + run.params.url);
var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
screenshot: run.params.screenshot ? ScreenshotHandler.getTmpFileRelativePath() : false,
device: run.params.device,
proxy: run.params.proxy,
waitForSelector: run.params.waitForSelector,
@@ -87,54 +84,54 @@ var ApiController = function(app) {
authPass: run.params.authPass,
blockDomain: run.params.blockDomain,
allowDomain: run.params.allowDomain,
noExternals: run.params.noExternals,
phantomasEngine: serverSettings.phantomasEngine
noExternals: run.params.noExternals
};
return ylt(run.params.url, runOptions);
return ylt(run.params.url, runOptions)
// Update the progress bar on each progress
.progress(function(progress) {
runsDatastore.updateRunProgress(run.runId, progress);
});
})
// Phantomas completed, let's save the screenshot if any
// Phantomas completed
.then(function(data) {
debug('Success');
data.runId = run.runId;
// Some conditional steps are made if there is a screenshot
// Some conditional steps exist if there is a screenshot
var screenshotPromise = Q.resolve();
if (run.params.screenshot) {
var screenshotSize = serverSettings.screenshotWidth ? serverSettings.screenshotWidth[run.params.device] : 400;
// Replace the empty promise created earlier with Q.resolve()
screenshotPromise = screenshot.toThumbnail(serverSettings.screenshotWidth || 400)
screenshotPromise = ScreenshotHandler.findAndOptimizeScreenshot(screenshotSize)
// Read screenshot
.then(function(screenshotBuffer) {
if (screenshotBuffer) {
debug('Image optimized');
data.screenshotBuffer = screenshotBuffer;
// Official path to get the image
data.screenshotUrl = 'api/results/' + data.runId + '/screenshot.jpg';
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
}
})
// Delete screenshot temporary file
.then(screenshot.deleteTmpFile)
// Don't worry if there's an error
.fail(function(err) {
debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
debug(err);
});
}
// Let's continue
screenshotPromise
return screenshotPromise
// Save results
.then(function() {
@@ -142,7 +139,8 @@ var ApiController = function(app) {
delete data.params.options.screenshot;
// Here we can remove tools results if not needed
delete data.toolsResults.phantomas.offenders.requests;
return resultsDatastore.saveResult(data);
})
@@ -226,10 +224,11 @@ var ApiController = function(app) {
}
});
// Retrieve the list of all runs
/*app.get('/api/runs', function(req, res) {
// NOT YET
});*/
// Counts all pending runs
app.get('/api/runs', function(req, res) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({pendingRuns: queue.length()}, null, 2));
});
// Delete one run by id
/*app.delete('/api/runs/:id', function(req, res) {
+5 -2
View File
@@ -1,7 +1,8 @@
var path = require('path');
var express = require('express');
var serverSettings = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
var serverSettings = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
var packageJson = require('../../../package.json');
var FrontController = function(app) {
'use strict';
@@ -10,13 +11,15 @@ var FrontController = function(app) {
var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
// Routes templating
var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
var routes = ['/', '/about', '/result/:runId', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
routes.forEach(function(route) {
app.get(route, function(req, res) {
res.setHeader('Cache-Control', 'public, max-age=20');
res.render(path.join(__dirname, assetsPath, 'main.html'), {
version: 'v' + packageJson.version,
baseUrl: app.locals.baseUrl || '/',
googleAnalyticsId: serverSettings.googleAnalyticsId,
sponsoring: serverSettings.sponsoring || {}
});
});
+25 -9
View File
@@ -16,10 +16,13 @@ function ResultsDatastore() {
this.saveResult = function(testResults) {
var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
var screenshotAPIPath = '/';
return createResultFolder(testResults.runId)
.then(function() {
return saveScreenshotIfExists(testResults);
return saveScreenshotIfExists(testResults, screenshotFilePath);
})
.then(function() {
@@ -56,10 +59,10 @@ function ResultsDatastore() {
// The folder /results/folderName/
function createResultFolder(folderName) {
var folder = path.join(resultsDir, folderName);
function createResultFolder(runId) {
var folder = path.join(resultsDir, runId);
debug('Creating the folder %s', folderName);
debug('Creating the folder %s', runId);
return createGlobalFolder().then(function() {
return Q.nfcall(fs.mkdir, folder);
@@ -89,22 +92,35 @@ function ResultsDatastore() {
return deferred.promise;
}
this.getResultFolder = function(runId) {
return path.join(resultsDir, runId);
};
// If there is a screenshot, save it as screenshot.jpg in the same folder as the results
function saveScreenshotIfExists(testResults) {
function saveScreenshotIfExists(testResults, path) {
var deferred = Q.defer();
if (testResults.screenshotBuffer) {
var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
fs.writeFile(screenshotFilePath, testResults.screenshotBuffer);
fs.writeFile(path, testResults.screenshotBuffer, function(err) {
if (err) {
debug('Could not save final screenshot');
debug(err);
// But it is OK, we don't need to fail the run
deferred.resolve();
} else {
debug('Final screenshot saved: ' + path);
deferred.resolve();
}
});
delete testResults.screenshotBuffer;
} else {
debug('Screenshot not found');
deferred.resolve();
}
return deferred;
return deferred.promise;
}
this.getScreenshot = function(runId) {
+10
View File
@@ -42,6 +42,16 @@ function RunsDatastore() {
};
// When the test is launched, set the progress bar
this.updateRunProgress = function(runId, progress) {
var run = runs[runId];
run.progress = progress;
runs[runId] = run;
};
this.markAsComplete = function(runId) {
var run = runs[runId];
+2 -2
View File
@@ -5,7 +5,7 @@ var parse = require('parse-color');
var colorDiff = function() {
var COLOR_DIFF_THRESHOLD = 0.9;
var COLOR_DIFF_THRESHOLD = 0.7;
this.compareAllColors = function(data) {
debug('Starting to compare all colors...');
@@ -55,7 +55,7 @@ var colorDiff = function() {
};
this.parseOffender = function(offender) {
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender.value.message);
return regexResult ? regexResult[1] : null;
};
+53
View File
@@ -0,0 +1,53 @@
var debug = require('debug')('ylt:domAccessAgregator');
var domAccessAgregator = function() {
'use strict';
this.agregate = function(data) {
debug('Starting to agregate DOM Accesses...');
let count = 0;
let offenders = {
byType: {}
};
const metricsToGather = [
'DOMqueriesById',
'DOMqueriesByTagName',
'DOMqueriesByClassName',
'DOMqueriesByQuerySelectorAll',
'DOMinserts',
'DOMmutationsInserts',
'DOMmutationsRemoves',
'DOMmutationsAttributes',
'eventsBound'
];
metricsToGather.forEach(key => {
if (data.toolsResults.phantomas.metrics[key]) {
count += data.toolsResults.phantomas.metrics[key];
}
offenders.byType[key] = [];
if (data.toolsResults.phantomas.offenders[key]) {
offenders.byType[key] = data.toolsResults.phantomas.offenders[key];
}
});
data.toolsResults.domAccessAgregator = {
metrics: {
DOMaccesses: count
},
offenders: {
DOMaccesses: offenders
}
};
debug('Done agregating DOM Accesses.');
return data;
};
};
module.exports = new domAccessAgregator();
-105
View File
@@ -1,105 +0,0 @@
var debug = require('debug')('ylt:isHttp2');
var url = require('url');
var Q = require('q');
var http2 = require('is-http2');
var isHttp2 = function() {
'use strict';
this.check = function(data) {
debug('Starting to check for HTTP2 support...');
return this.checkHttp2(data)
.then(function(result) {
if (result.isHttp) {
debug('The website is not even in HTTPS');
data.toolsResults.http2 = {
metrics: {
http2: false
}
};
} else if (result.isHttp2) {
debug('HTTP/2 (or SPDY) is supported');
data.toolsResults.http2 = {
metrics: {
http2: true
}
};
} else {
debug('HTTP/2 is not supported');
data.toolsResults.http2 = {
metrics: {
http2: false
}
};
}
// Add the supported protocols as offenders
if (result.supportedProtocols) {
debug('Supported protocols: ' + result.supportedProtocols.join(' '));
data.toolsResults.http2.offenders = {
http2: result.supportedProtocols
};
}
debug('End of HTTP2 support check');
return data;
})
.fail(function() {
return data;
});
};
this.getParsedUrl = function(data) {
return url.parse(data.toolsResults.phantomas.url);
};
this.getProtocol = function(data) {
return this.getParsedUrl(data).protocol;
};
this.getDomain = function(data) {
return this.getParsedUrl(data).hostname;
};
this.checkHttp2 = function(data) {
var deferred = Q.defer();
// Check if it's HTTPS first
if (this.getProtocol(data) === 'http:') {
deferred.resolve({
isHttp: true
});
} else {
// To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
http2(this.getDomain(data), {includeSpdy: true})
.then(function(result) {
deferred.resolve(result);
})
.catch(function(error) {
debug('Error while checking HTTP2 support:');
debug(error);
deferred.reject('Error while checking for HTTP2 support');
});
}
return deferred.promise;
};
};
module.exports = new isHttp2();
-210
View File
@@ -1,210 +0,0 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var Collection = require('./phantomas/custom_modules/util/collection');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var jQueryFunctionsCollection = new Collection();
var metrics = {
domInteractive: 0,
domContentLoaded: 0,
domContentLoadedEnd: 0,
domComplete: 0,
DOMaccesses: 0,
DOMaccessesOnScroll: 0,
queriesWithoutResults: 0
};
var offenders = {};
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
if (hasjQuery) {
metrics.jQueryCalls = 0;
metrics.jQueryCallsOnEmptyObject = 0;
metrics.jQueryNotDelegatedEvents = 0;
}
try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node, index) {
var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if (isABindWithoutEventDelegation(node, contextLength)) {
metrics.jQueryNotDelegatedEvents += contextLength;
node.warning = true;
node.eventNotDelegated = true;
}
if (node.data.resultsNumber === 0) {
metrics.queriesWithoutResults ++;
node.queryWithoutResults = true;
node.warning = true;
}
if (contextLength === 0) {
metrics.jQueryCallsOnEmptyObject ++;
node.jQueryCallOnEmptyObject = true;
node.warning = true;
}
if (node.data.type.indexOf('jQuery - ') === 0) {
metrics.jQueryCalls ++;
jQueryFunctionsCollection.push(node.data.type);
}
// Mark errors with an error flag
if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
node.error = true;
}
// Mark a performance flag
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
// Adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
javascriptExecutionTree.data.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
javascriptExecutionTree.data.domComplete = node.data.timestamp;
break;
}
}
// Fix rare bug when domComplete was never triggered
if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccesses += countTreeLeafs(node);
});
}
debug('JS execution transformation complete');
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);
});
}
debug('Scroll execution transformation complete');
} else {
debug('Could not parse scrollExecutionTree');
}
} catch(err) {
throw err;
}
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics,
offenders: offenders
};
return data;
};
function treeRecursiveParser(node, fn) {
if (node.children) {
node.children.forEach(function(child) {
treeRecursiveParser(child, fn);
});
}
fn(node);
}
function changeListOfDomPaths(rootNode) {
treeRecursiveParser(rootNode, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
}
if (node.data.type === 'insertBefore') {
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
}
});
}
// Returns the number of leafs (nodes without children)
function countTreeLeafs(rootNode) {
var count = 0;
treeRecursiveParser(rootNode, function(node) {
if (!node.children &&
!node.error &&
!node.windowPerformance &&
node.data.type !== 'jQuery loaded') {
count ++;
}
});
return count;
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
}
function isABindWithoutEventDelegation(node, contextLength) {
// Count only on larger bindings
if (contextLength <= 3) {
return false;
}
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
return true;
}
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
var child = node.children[0];
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
return true;
}
}
return false;
}
};
module.exports = new jsExecutionTransformer();
@@ -1,251 +0,0 @@
/**
* Overwritting the original spying functions in scope.js
* This is done so we now have a before AND an after callback on the spy
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
var responseEndTime = Date.now();
phantomas.on('responseEnd', function() {
responseEndTime = Date.now();
});
phantomas.on('init', function() {
phantomas.evaluate(function(responseEndTime) {
(function(phantomas) {
// Overwritting phantomas spy function
(function() {
var enabled = true;
// turn off spying to not include internal phantomas actions
function spyEnabled(state, reason) {
enabled = (state === true);
phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : ''));
}
phantomas.log('Overwritting phantomas spy function');
function spy(obj, fn, callbackBefore, callbackAfter) {
var origFn = obj && obj[fn];
if (typeof origFn !== 'function') {
return false;
}
phantomas.log('Attaching a YLT spy to "' + fn + '" function...');
obj[fn] = function() {
var result;
var err;
// Before
if (enabled && callbackBefore) {
callbackBefore.apply(this, arguments);
}
// Execute
try {
result = origFn.apply(this, arguments);
} catch(e) {
// Catching the err for the moment, because we need to make sure the callbackAfter function is called.
phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
phantomas.log(arguments);
err = e;
} finally {
// After
if (enabled && callbackAfter) {
var args = Array.prototype.slice.call(arguments);
callbackAfter.apply(this, [result].concat(args));
}
if (err) {
phantomas.log('Re-throwing the error');
throw err;
}
}
return result;
};
// copy custom properties of original function to the mocked one
Object.keys(origFn).forEach(function(key) {
obj[fn][key] = origFn[key];
});
obj[fn].prototype = origFn.prototype;
return true;
}
phantomas.spyEnabled = spyEnabled;
phantomas.spy = spy;
})();
// Adding some code for the Javascript execution tree construction
(function() {
var root = new ContextTreeNode(null, {type: 'main'});
var currentContext = root;
var depth = 0;
// Add a child but don't enter its context
function pushContext(data) {
// Some data is not needed on subchildren
if (depth === 0) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
}
// Some data is not needed on subchildren
if (depth > 0) {
if (data.backtrace) {
delete data.backtrace;
}
if (data.resultsNumber) {
delete data.resultsNumber;
}
}
currentContext.addChild(data);
}
// Add a child to the current context and enter its context
function enterContext(data) {
// Some data is not needed on subchildren
if (depth === 0) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
}
// Some data is not needed on subchildren
if (depth > 0) {
if (data.backtrace) {
delete data.backtrace;
}
if (data.resultsNumber) {
delete data.resultsNumber;
}
}
currentContext = currentContext.addChild(data);
depth ++;
}
// Save given data in the current context and jump change current context to its parent
function leaveContext(moreData) {
// Some data is not needed on subchildren
if (depth === 1) {
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
}
// Some data is not needed on subchildren
if (depth > 1) {
if (moreData && moreData.backtrace) {
delete moreData.backtrace;
}
if (moreData && moreData.resultsNumber) {
delete moreData.resultsNumber;
}
}
// Merge previous data with moreData (ovewrites if exists)
if (moreData) {
for (var key in moreData) {
currentContext.data[key] = moreData[key];
}
}
var parent = currentContext.parent;
if (parent === null) {
console.error('Error: trying to close root context in ContextTree');
} else {
currentContext = parent;
}
depth --;
}
function getContextData() {
return currentContext.data;
}
// Returns a clean object, without the parent which causes recursive loops
function readFullTree() {
// Return null if the contextTree is not correctly closed
if (root !== currentContext) {
return null;
}
function recusiveRead(node) {
if (node.children.length === 0) {
delete node.children;
} else {
for (var i=0, max=node.children.length ; i<max ; i++) {
recusiveRead(node.children[i]);
}
}
delete node.parent;
}
recusiveRead(root);
return root;
}
// Empty the tree
function resetTree() {
root = new ContextTreeNode(null, {type: 'main'});
currentContext = root;
depth = 0;
}
function ContextTreeNode(parent, data) {
this.data = data;
this.parent = parent;
this.children = [];
this.addChild = function(data) {
var child = new ContextTreeNode(this, data);
this.children.push(child);
return child;
};
}
phantomas.log('Adding some contextTree functions to phantomas');
phantomas.pushContext = pushContext;
phantomas.enterContext = enterContext;
phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree;
phantomas.resetTree = resetTree;
})();
})(window.__phantomas);
}, responseEndTime);
});
};
@@ -1,30 +0,0 @@
/**
* Analyzes AJAX requests
*/
/* global window: true */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('ajaxRequests'); // @desc number of AJAX requests
phantomas.setMetric('synchronousXHR'); // @desc number of synchronous
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spy(window.XMLHttpRequest.prototype, 'open', null, function(result, method, url, async) {
phantomas.incrMetric('ajaxRequests');
phantomas.addOffender('ajaxRequests', '<%s> [%s]', url, method);
if (async === false) {
phantomas.incrMetric('synchronousXHR');
phantomas.addOffender('synchronousXHR', url);
phantomas.log('ajaxRequests: synchronous XMLHttpRequest call to <%s>', url);
}
}, true);
})(window.__phantomas);
});
});
};
@@ -1,86 +0,0 @@
/**
* Analyzes DOM hidden content
*/
/* global document: true, Node: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// total length of HTML of hidden elements (i.e. display: none)
phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders
phantomas.setMetric('hiddenImages'); // @desc number of hidden images that can be lazy-loaded @offenders
// HTML size
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var runner = new phantomas.nodeRunner(),
lazyLoadableImages = {};
phantomas.spyEnabled(false, 'analyzing hidden content');
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
// @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
var styles = window.getComputedStyle(node);
if (styles && styles.getPropertyValue('display') === 'none') {
if (typeof node.innerHTML === 'string') {
var size = node.innerHTML.length;
phantomas.incrMetric('hiddenContentSize', size);
// log hidden containers bigger than 1 kB
if (size > 1024) {
phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
}
}
// count hidden images that can be lazy loaded (issue #524)
var images = [];
if (node.tagName === 'IMG') {
images = [node];
} else if (typeof node.querySelectorAll === 'function') {
images = node.querySelectorAll('img') || [];
}
for (var i = 0, len = images.length; i < len; i++) {
var src = images[i].src,
path;
if (src === '' || src.indexOf('data:image') === 0) continue;
if (images[i].width === 1 && images[i].height === 1) continue;
if (!lazyLoadableImages[src]) {
path = phantomas.getDOMPath(images[i]);
lazyLoadableImages[src] = {
path: path
};
}
}
// don't run for child nodes as they're hidden as well
return false;
}
break;
}
});
Object.keys(lazyLoadableImages).forEach(function(img) {
var entry = lazyLoadableImages[img];
phantomas.incrMetric('hiddenImages');
phantomas.addOffender('hiddenImages', img);
phantomas.log('hiddenImages: <%s> image (%s) is hidden and can be lazy-loaded', img, entry.path);
});
phantomas.spyEnabled(true);
}(window.__phantomas));
});
});
};
@@ -1,387 +0,0 @@
/**
* Analyzes DOM queries done via native DOM methods
*/
/* global Element: true, Document: true, Node: true, window: true */
exports.version = '0.10.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @offenders
phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
// fake native DOM functions
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function querySpy(type, query, fnName, context, hasNoResults) {
phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made
}
phantomas.spy(Document.prototype, 'getElementById', function(id) {
phantomas.incrMetric('DOMqueriesById');
phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
phantomas.enterContext({
type: 'getElementById',
callDetails: {
arguments: ['#' + id]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
var id = args ? '#' + args[0] : undefined;
querySpy('id', id, 'getElementById', '#document', (result === null));
var moreData = {
resultsNumber : (result === null) ? 0 : 1
};
phantomas.leaveContext(moreData);
});
// selectors by class name
function selectorClassNameSpyBefore(className) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByClassName');
phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
phantomas.enterContext({
type: 'getElementsByClassName',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: ['.' + className]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorClassNameAfter(result, args) {
/*jshint validthis: true */
var className = args ? '.' + args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('class', className, 'getElementsByClassName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
// selectors by tag name
function selectorTagNameSpyBefore(tagName) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByTagName');
phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
phantomas.enterContext({
type: 'getElementsByTagName',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorTagNameSpyAfter(result, args) {
/*jshint validthis: true */
var tagName = args ? args[0].toLowerCase() : undefined;
var context = phantomas.getDOMPath(this);
querySpy('tag name', tagName, 'getElementsByTagName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
// selector queries
function selectorQuerySpy(selector, context) {
phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
}
function selectorQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelector',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorQuerySpyAfter(result, args) {
/*jshint validthis: true */
var selector = args ? args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : result ? 1 : 0
};
phantomas.leaveContext(moreData);
}
function selectorAllQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelectorAll',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorAllQuerySpryAfter(result, args) {
/*jshint validthis: true */
var selector = args ? args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
// count DOM inserts
function appendChild(child, element, context, appended) {
/*jshint validthis: true */
// ignore appending to the node that's not yet added to DOM tree
if (!element.parentNode) {
return;
}
// don't count elements added to fragments as a DOM inserts (issue #350)
// DocumentFragment > div[0]
if (context.indexOf('DocumentFragment') === 0) {
return;
}
phantomas.incrMetric('DOMinserts');
phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context);
//phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context);
}
function appendChildSpyBefore(child) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'appendChild',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [appended]
},
backtrace: phantomas.getBacktrace()
});
}
function insertBeforeSpyBefore(child, refElement) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
var referent = phantomas.getDOMPath(refElement);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'insertBefore',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [
appended,
referent
]
},
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createElement', function(tagName) {
phantomas.enterContext({
type: 'createElement',
callDetails: {
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createTextNode', function(text) {
phantomas.enterContext({
type: 'createTextNode',
callDetails: {
arguments: [text]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
phantomas.enterContext({
type: 'createDocumentFragment',
callDetails: {
arguments: []
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
var target = phantomas.getDOMPath(element);
phantomas.enterContext({
type: 'getComputedStyle',
callDetails: {
arguments: [target, pseudoElement]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
})(window.__phantomas);
});
});
// report DOM queries that return no results (issue #420)
phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) {
// ignore DOM queries within DOM fragments (used internally by jQuery)
if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) {
return;
}
if (hasNoResults === true) {
phantomas.incrMetric('DOMqueriesWithoutResults');
phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName);
}
});
// count DOM queries by either ID, tag name, class name and selector query
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
var Collection = require('../../util/collection'),
DOMqueries = new Collection();
phantomas.on('domQuery', function(type, query, fnName, context) {
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
phantomas.incrMetric('DOMqueries');
if (context && (
context.indexOf('html') === 0 ||
context.indexOf('body') === 0 ||
context.indexOf('head') === 0 ||
context.indexOf('#document') === 0
)) {
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
}
});
phantomas.on('report', function() {
DOMqueries.sort().forEach(function(query, cnt) {
if (cnt > 1) {
phantomas.incrMetric('DOMqueriesDuplicated');
phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1);
phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
}
});
});
};
@@ -1,82 +0,0 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.4.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.log('event ' + eventType + ' bound');
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
length: 1,
elements: [path]
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
// count window.addEventListener('scroll', ...) - issue #508
if (eventType === 'scroll' && (path === 'window' || path === '#document')) {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path);
}
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
// spy calls to EventTarget.dispatchEvent
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent
phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path);
phantomas.incrMetric('eventsDispatched');
phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path);
});
})(window.__phantomas);
});
});
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// Check if a window.onscroll function is defined
if (typeof(window.onscroll) === "function") {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll');
}
}(window.__phantomas));
});
});
};
@@ -1,381 +0,0 @@
/**
* Analyzes jQuery activity
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
/* jshint -W030 */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version)
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
var jQueryFunctions = [
// DOM manipulations
'html',
'append',
'appendTo',
'prepend',
'prependTo',
'before',
'insertBefore',
'after',
'insertAfter',
'remove',
'detach',
'empty',
'clone',
'replaceWith',
'replaceAll',
'text',
'wrap',
'wrapAll',
'wrapInner',
'unwrap',
// Style manipulations
'css',
'offset',
'position',
'height',
'innerHeight',
'outerHeight',
'width',
'innerWidth',
'outerWidth',
'scrollLeft',
'scrollTop',
// Animations
'hide',
'show',
'toggle',
'animate',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'slideDown',
'slideUp',
'slideToggle',
// Generic events
'on',
'off',
'live',
'die',
'delegate',
'undelegate',
'one',
'bind',
'unbind',
// More events
'blur',
'change',
'click',
'dblclick',
'error',
'focus',
'focusin',
'focusout',
'hover',
'keydown',
'keypress',
'keyup',
'load',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
'resize',
'scroll',
'select',
'submit',
'unload',
// Attributes
'attr',
'prop',
'removeAttr',
'removeProp',
'val',
'hasClass',
'addClass',
'removeClass',
'toggleClass',
];
var jQueryTraversalFunctions = [
'children',
'closest',
'find',
'next',
'nextAll',
'nextUntil',
'offsetParent',
'parent',
'parents',
'parentsUntil',
'prev',
'prevAll',
'prevUntil',
'siblings'
];
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
// spy calls to jQuery functions
phantomas.on('init', function() {
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
(function(phantomas) {
var oldJQuery;
phantomas.spyGlobalVar('jQuery', function(jQuery) {
var version;
if (!jQuery || !jQuery.fn) {
phantomas.log('jQuery: unable to detect version!');
return;
}
// Tag the current version of jQuery to avoid multiple reports of jQuery being loaded
// when it's actually only restored via $.noConflict(true) - see comments in #435
if (jQuery.__phantomas === true) {
phantomas.log('jQuery: this instance has already been seen by phantomas');
return;
}
jQuery.__phantomas = true;
// report the version of jQuery
version = jQuery.fn.jquery;
phantomas.emit('jQueryLoaded', version);
phantomas.pushContext({
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
callDetails: {
arguments: ['version ' + version]
},
backtrace: phantomas.getBacktrace()
});
oldJQuery = version;
// jQuery.ready.promise
// works for jQuery 1.8.0+ (released Aug 09 2012)
phantomas.spy(jQuery.ready, 'promise', function(func) {
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
phantomas.pushContext({
type: 'jQuery - onDOMReady',
callDetails: {
arguments: [func]
},
backtrace: phantomas.getBacktrace()
});
}) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
// Sizzle calls - jQuery.find
// works for jQuery 1.3+ (released Jan 13 2009)
phantomas.spy(jQuery, 'find', function(selector, context) {
phantomas.incrMetric('jQuerySizzleCalls');
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
phantomas.enterContext({
type: 'jQuery - Sizzle call',
callDetails: {
context: {
length: 1,
elements: [phantomas.getDOMPath(context)]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
phantomas.spy(jQuery.fn, 'init', function(selector, context) {
if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
phantomas.enterContext({
type: 'jQuery - find',
callDetails: {
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
}, function(result) {
var data = phantomas.getContextData();
if (data.type === 'jQuery - find' &&
!data.callDetails.context &&
data.callDetails.arguments.length === 1 &&
/^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
});
if (!jQuery.event) {
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
var path = phantomas.getDOMPath(elem),
type = ev.type || ev;
phantomas.log('Event: triggered "%s" on "%s"', type, path);
phantomas.incrMetric('jQueryEventTriggers');
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
});
}
// jQuery events bound to window' onLoad event (#451)
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
if ((eventName === 'load') && (this[0] === window)) {
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
}
});
// Add spys on many jQuery functions
jQueryFunctions.forEach(function(functionName) {
phantomas.spy(jQuery.fn, functionName, function(args) {
// Clean args
args = [].slice.call(arguments);
args.forEach(function(arg, index) {
if (arg instanceof Array) {
arg = '[Array]';
}
if (arg instanceof Object) {
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
arg = phantomas.getDOMPath(arg[0]) || 'unknown';
} else if (arg instanceof HTMLElement) {
arg = phantomas.getDOMPath(arg) || 'unknown';
} else if (typeof arg === 'function') {
arg = '(function)';
} else {
try {
for (var key in arg) {
if (typeof arg[key] === 'function') {
arg[key] = '(function)';
}
}
arg = JSON.stringify(arg);
} catch(e) {
arg = '[Object]';
}
}
}
if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) {
arg = arg.substring(0, 200) + '...';
}
if (typeof arg === 'function') {
arg = '(function)';
}
if (arg === true) {
arg = 'true';
}
if (arg === false) {
arg = 'false';
}
if (arg === null) {
arg = 'null';
}
if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
arg = 'undefined';
}
args[index] = arg;
});
var elements = [];
for (var i = 0 ; i < this.length ; i++) {
elements.push(phantomas.getDOMPath(this[i]));
}
phantomas.enterContext({
type: 'jQuery - ' + functionName,
callDetails: {
context: {
length: this.length,
elements: elements
},
arguments: args
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
} else {
phantomas.leaveContext();
}
}) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
});
});
})(window.__phantomas);
}, jQueryFunctions, jQueryTraversalFunctions);
});
phantomas.on('jQueryLoaded', function(version) {
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
// report multiple jQuery "instances" (issue #435)
phantomas.incrMetric('jQueryVersionsLoaded');
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
});
};
@@ -1,69 +0,0 @@
/**
* Reports the use of functions known to be serious performance bottlenecks in JS
*
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
* @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
*
* Run phantomas with --spy-eval to count eval() calls (see issue #467)
*/
/* global document: true, window: true */
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders
// spy calls to eval only when requested (issue #467)
var spyEval = phantomas.getParam('spy-eval') === true;
if (!spyEval) {
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
}
phantomas.on('init', function() {
phantomas.evaluate(function(spyEval) {
(function(phantomas) {
function report(msg, caller, backtrace, metric) {
phantomas.log(msg + ': from ' + caller + '!');
phantomas.log('Backtrace: ' + backtrace);
phantomas.incrMetric(metric);
phantomas.addOffender(metric, "%s from %s", msg, caller);
}
// spy calls to eval()
if (spyEval) {
phantomas.spy(window, 'eval', function(code) {
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
});
}
// spy calls to setTimeout / setInterval with string passed instead of a function
phantomas.spy(window, 'setTimeout', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
phantomas.spy(window, 'setInterval', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
// spy document.write(ln)
phantomas.spy(document, 'write', function(arg) {
report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
phantomas.spy(document, 'writeln', function(arg) {
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
})(window.__phantomas);
}, spyEval);
});
};
@@ -1,49 +0,0 @@
/**
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
*/
exports.version = '0.3.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors
function formatTrace(trace) {
var ret = [];
if(Array.isArray(trace)) {
trace.forEach(function(entry) {
ret.push((entry.function ? entry.function + ' ' : '') + (entry.sourceURL || entry.file) + ':' + entry.line);
});
}
return ret;
}
phantomas.on('jserror', function(msg, trace) {
trace = formatTrace(trace);
phantomas.log(msg);
phantomas.log('Backtrace: ' + trace.join(' / '));
phantomas.incrMetric('jsErrors');
phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / '));
// Yeah, this is weird, i'm sending the error back to the browser...
phantomas.evaluate(function(msg, caller, trace) {
(function(phantomas) {
phantomas.pushContext({
type: 'error',
callDetails: {
arguments: [msg]
},
caller: caller,
backtrace: trace
});
})(window.__phantomas);
}, msg, trace[0], trace.join(' / '));
});
};
@@ -1,31 +0,0 @@
/**
* Saves the javascript interractions with the DOM
*
* Run phantomas with --js-execution-tree option to use this module
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('javascriptExecutionTree');
// save data
phantomas.on('report', function() {
phantomas.log('JS execution tree: Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree();
if (fullTree === null) {
phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
return;
}
phantomas.setMetric('javascriptExecutionTree', true, true);
phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
})(window.__phantomas);});
});
};
@@ -1,81 +0,0 @@
/**
* 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,
// Add an offset of 100px under the height of the screen
LAZYLOAD_OFFSET = 100;
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 + LAZYLOAD_OFFSET) {
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);
});
});
};
@@ -1,23 +0,0 @@
/**
* Retries download on every request to get the real file size
*
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('requestsList');
var requests = [];
phantomas.on('recv', function(entry, res) {
requests.push(entry);
});
phantomas.on('report', function() {
phantomas.setMetric('requestsList', true, true);
phantomas.addOffender('requestsList', JSON.stringify(requests));
});
};
@@ -1,69 +0,0 @@
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('scrollExecutionTree');
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('scroll', false, false, null);
function triggerScrollEvent() {
phantomas.resetTree();
try {
// Chrome triggers them in this order:
// 1. document
phantomas.pushContext({
type: 'documentScroll'
});
document.dispatchEvent(evt);
// 2. window
phantomas.pushContext({
type: 'windowScroll'
});
window.dispatchEvent(evt);
// No need to call window.onscroll(), it's called by the scroll event on window
} catch(e) {
phantomas.log('ScrollListener error: %s', e);
}
}
var firstScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a first scroll event...');
triggerScrollEvent();
// Ignore the first scroll event and only save the second one,
// because we want to detect un-throttled things, throttled ones are ok.
var secondScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
triggerScrollEvent();
var fullTree = phantomas.readFullTree();
if (fullTree !== null) {
phantomas.setMetric('scrollExecutionTree', true, true);
phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
} else {
phantomas.log('Error: scrollExecutionTree could not be extracted');
}
phantomas.log('ScrollListener: end of scroll triggering');
})(window.__phantomas);
});
});
};
@@ -1,150 +0,0 @@
/**
* Measure when the page reaches certain states
*
* @see http://w3c-test.org/webperf/specs/NavigationTiming/#dom-performancetiming-domloading
* @see https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp
*/
/* global document: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// times below are calculated relative to performance.timing.responseEnd (#117)
phantomas.setMetric('domInteractive'); // @desc time it took to parse the HTML and construct the DOM
phantomas.setMetric('domContentLoaded'); // @desc time it took to construct both DOM and CSSOM, no stylesheets that are blocking JavaScript execution (i.e. onDOMReady)
phantomas.setMetric('domContentLoadedEnd'); // @desc time it took to finish handling of onDOMReady event @unreliable
phantomas.setMetric('domComplete'); // @desc time it took to load all page resources, the loading spinner has stopped spinning
// backend vs frontend time
phantomas.setMetric('timeBackend'); // @desc time to the first byte compared to the total loading time [%]
phantomas.setMetric('timeFrontend'); // @desc time to window.load compared to the total loading time [%]
// measure dom... metrics from the moment HTML response was fully received
var responseEndTime = Date.now();
phantomas.on('responseEnd', function() {
responseEndTime = Date.now();
phantomas.log('Performance timing: responseEnd = %d', responseEndTime);
});
phantomas.on('init', function() {
phantomas.evaluate(function(responseEndTime) {
(function(phantomas) {
phantomas.spyEnabled(false, 'installing window.performance metrics');
phantomas.currentStep = 'domCreation';
// extend window.performance
// "init" event is sometimes fired twice, pass a value set by "responseEnd" event handler (fixes #192)
if (typeof window.performance === 'undefined') {
window.performance = {
timing: {
responseEnd: responseEndTime
}
};
phantomas.log('Performance timing: emulating window.performance');
}
else {
phantomas.log('Performance timing: using native window.performance');
}
// onDOMReady
document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() {
// use NavigationTiming if possible
var time = window.performance.timing.domContentLoadedEventEnd ?
(window.performance.timing.domContentLoadedEventEnd - window.performance.timing.responseEnd)
:
(Date.now() - responseEndTime);
phantomas.currentStep = 'domContentLoadedEnd';
phantomas.setMetric('domContentLoadedEnd', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoadedEnd" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoadedEnd'
});
}, 0);
var time = Date.now() - responseEndTime;
phantomas.currentStep = 'domContentLoaded';
phantomas.setMetric('domContentLoaded', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoaded" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoaded'
});
});
// emulate Navigation Timing
document.addEventListener('readystatechange', function() {
var readyState = document.readyState,
responseEndTime = window.performance.timing.responseEnd,
time = Date.now() - responseEndTime,
metricName;
// @see http://www.w3.org/TR/html5/dom.html#documentreadystate
switch(readyState) {
// the browser has finished parsing all of the HTML and DOM construction is complete
case 'interactive':
metricName = 'domInteractive';
break;
// the processing is complete and all of the resources on the page have finished downloading
case 'complete':
metricName = 'domComplete';
phantomas.log('Performance timing: %j', window.performance.timing);
break;
default:
phantomas.log('Performance timing: unhandled "%s" state!', readyState);
return;
}
phantomas.currentStep = metricName;
phantomas.setMetric(metricName, time, true);
phantomas.log('Performance timing: document reached "%s" state after %d ms', readyState, time);
phantomas.pushContext({
type: metricName
});
});
phantomas.spyEnabled(true);
})(window.__phantomas);
}, responseEndTime);
});
/**
* Emit metrics with backend vs frontend time
*
* Performance Golden Rule:
* "80-90% of the end-user response time is spent on the frontend. Start there."
*
* @see http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
*/
phantomas.on('report', function() {
// The “backend” time is the time it takes the server to get the first byte back to the client.
// The “frontend” time is measured from the last byte of the response (responseEnd) until all resources are fetched (domComplete)
var backendTime = parseInt(phantomas.getMetric('timeToFirstByte'), 10),
frontendTime = parseInt(phantomas.getMetric('domComplete'), 10),
totalTime = backendTime + frontendTime,
backendTimePercentage;
if (totalTime === 0) {
return;
}
backendTimePercentage = Math.round(backendTime / totalTime * 100);
phantomas.setMetric('timeBackend', backendTimePercentage);
phantomas.setMetric('timeFrontend', 100 - backendTimePercentage);
phantomas.log('Performance timing: backend vs frontend time - %d% / %d%', backendTimePercentage, 100 - backendTimePercentage);
});
};
+42 -133
View File
@@ -17,16 +17,27 @@ var PhantomasWrapper = function() {
var deferred = Q.defer();
var task = data.params;
var viewportOption = null;
// Setting screen dimensions for desktop devices only.
// Phone and tablet dimensions are dealt by Phantomas.
if (task.options.device === 'desktop') {
// Similar to an old non-retina Macbook Air 13"
viewportOption = '1280x800x1';
} else if (task.options.device === 'desktop-hd') {
// Similar to a retina Macbook Pro 16"
viewportOption = '1536x960x2';
}
var options = {
// Cusomizable options
'engine': task.options.phantomasEngine || 'webkit',
'timeout': task.options.timeout || 30,
'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,
'timeout': task.options.timeout || 120,
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/85.0.4183.121 Safari/537.36' : null,
'tablet': (task.options.device === 'tablet'),
'phone': (task.options.device === 'phone'),
'screenshot': task.options.screenshot || false,
'wait-for-selector': task.options.waitForSelector,
'viewport': viewportOption,
'wait-for-network-idle': true,
'cookie': task.options.cookie,
'auth-user': task.options.authUser,
'auth-pass': task.options.authPass,
@@ -35,151 +46,49 @@ var PhantomasWrapper = function() {
'no-externals': task.options.noExternals,
// Mandatory
'reporter': 'json:pretty',
'analyze-css': true,
'ignore-ssl-errors': true,
'skip-modules': [
'ajaxRequests', // overridden
'domHiddenContent', // overridden
'domMutations', // not compatible with webkit
'domQueries', // overridden
'events', // overridden
'filmStrip', // not needed
'har', // not needed for the moment
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
'jQuery', // overridden
'jserrors', // overridden
'lazyLoadableImages', //overridden
'pageSource', // not needed
'windowPerformance' // overridden
].join(','),
'include-dirs': [
path.join(__dirname, 'custom_modules/core'),
path.join(__dirname, 'custom_modules/modules')
].join(',')
'ignoreSslErrors': true
};
// Proxy option can't be set to null or undefined...
// this is why it's set now and not in the object above
if (task.options.proxy) {
options.proxy = task.options.proxy;
}
// Output the command line for debugging purpose
debug('If you want to reproduce the phantomas task only, copy the following command line:');
var optionsString = '';
for (var opt in options) {
var value = options[opt];
if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
value = '"' + value + '"';
var debugCmd = 'DEBUG=* node node_modules/phantomas/bin/phantomas.js --url ' + task.url;
Object.keys(options).forEach(function(key) {
if (key !== 'wait-for-network-idle' && options[key] !== null) {
debugCmd += ' --' + key + ' ' + options[key];
}
if (value === true) {
optionsString += ' ' + '--' + opt;
} else if (value === false || value === null || value === undefined) {
// Nothing
} else {
optionsString += ' ' + '--' + opt + '=' + value;
}
}
debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
var phantomasPid;
var isKilled = false;
// Kill phantomas if nothing happens
var killer = setTimeout(function() {
console.log('Killing phantomas because the test on ' + task.url + ' was launched ' + 5*options.timeout + ' seconds ago');
if (phantomasPid) {
ps.kill(phantomasPid, function(err) {
if (err) {
debug('Could not kill Phantomas process %s', phantomasPid);
// Suicide
process.exit(1);
// If in server mode, forever will restart the server
}
debug('Phantomas process %s was correctly killed', phantomasPid);
// Then mark the test as failed
// Error 1003 = Phantomas not answering
deferred.reject(1003);
isKilled = true;
});
} else {
// Suicide
process.exit(1);
}
}, 5*options.timeout*1000);
});
debug('If you want to run Phantomas alone for debugging purpose, this is the command: %s', debugCmd);
// It's time to launch the test!!!
var triesNumber = 2;
var currentTry = 0;
const promise = phantomas(task.url, options);
async.retry(triesNumber, function(cb) {
currentTry ++;
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...');
return;
}
debug('Returning from Phantomas with error %s', errorCode);
// Adding some YellowLabTools errors here
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
errorCode = 1001;
}
if (!errorCode && (!json || !json.metrics)) {
errorCode = 1002;
}
// Don't cancel test if it is a timeout and we've got some results
if (errorCode === 252 && json) {
debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
errorCode = null;
}
if (errorCode) {
debug('Attempt failed. Error code ' + errorCode);
}
cb(errorCode, json);
}).fail(function() {
// This function is useless, but the failing promise needs to be handled,
// otherwise the module meow writes in the console in case of a timeout (error code 252).
debug('Failing promise handled');
});
phantomasPid = process.pid;
}, function(err, json) {
clearTimeout(killer);
if (err) {
debug('All ' + triesNumber + ' attemps failed for the test');
deferred.reject(err);
} else {
// handle the promise
promise.
then(results => {
var json = {
generator: results.getGenerator(),
url: results.getUrl(),
metrics: results.getMetrics(),
offenders: results.getAllOffenders()
};
deferred.resolve(json);
}).
catch(res => {
console.error(res);
deferred.reject('Phantomas failed: ' + res.message);
});
promise.on('milestone', function(event) {
if (event === 'domReady' || event === 'domComplete') {
deferred.notify(event);
}
});
+107
View File
@@ -0,0 +1,107 @@
var debug = require('debug')('ylt:brotliCompressor');
var Q = require('q');
var zlib = require('zlib');
var gzipCompressor = require('./gzipCompressor');
var GzipCompressor = function() {
var BROTLI_COMPRESSION_LEVEL = 9;
function compressFile(entry) {
debug('Entering brotli compressor');
return brotlifyFile(entry)
.then(brotliOptimizedFile);
}
// Compress with Brotli files that were not compressed or not with brotli (with gzip or deflate)
function brotlifyFile(entry) {
var deferred = Q.defer();
if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.bodyBuffer) {
if (!entry.weightCheck.isCompressed) {
debug('File %s was not compressed at all, trying Brotli over it.', entry.url);
} else if (entry.weightCheck.compressionTool !== 'brotli') {
debug('File %s was compressed with %s. Trying with Brotli.', entry.url, entry.weightCheck.compressionTool);
}
zlib.brotliCompress(entry.weightCheck.bodyBuffer, {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 9
}
}, function(err, buffer) {
if (err) {
debug('Could not compress uncompressed file with brotli');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
if (!entry.weightCheck.isCompressed) {
debug('Brotli size is %d, was %d, this is %d% better.', compressedSize, entry.weightCheck.bodySize, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
} else if (entry.weightCheck.compressionTool !== 'brotli') {
debug('Brotli size is %d, was %d with %s, this is %d% better.', compressedSize, entry.weightCheck.bodySize, entry.weightCheck.compressionTool, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
}
entry.weightCheck.afterBrotliCompression = compressedSize;
deferred.resolve(entry);
}
}
);
} else {
debug('Compression not needed');
deferred.resolve(entry);
}
return deferred.promise;
}
// Gzip a file after minification or optimization if this step was successful
function brotliOptimizedFile(entry) {
var deferred = Q.defer();
if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
debug('Trying to brotlify file after minification: %s', entry.url);
var uncompressedSize = entry.weightCheck.optimized;
zlib.brotliCompress(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 9
}
}, function(err, buffer) {
if (err) {
debug('Could not compress minified file with brotli');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
debug('Correctly brotlified the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterOptimizationAndBrotliCompression = compressedSize;
deferred.resolve(entry);
}
}
);
} else {
debug('Compressing optimized file not needed');
deferred.resolve(entry);
}
return deferred.promise;
}
return {
compressFile: compressFile
};
};
module.exports = new GzipCompressor();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
var debug = require('debug')('ylt:cdnDetector');
var Q = require('q');
var url = require('url');
var dns = require('dns');
var cdnDetector = require('cdn-detector');
// This module checks my multiple ways if a request was served by a CDN
var CDNDetector = function() {
function detectCDN(entry) {
var urlObject = url.parse(entry.url);
debug('Start checking CDN for %s, host is %s', entry.url, urlObject.host);
// 1st, the fastest check: the domain is directly recognized as a CDN
return checkDomainDirectly(entry)
.fail(function() {
// 2nd, check the response headers
return checkHeaders(entry)
.fail(function() {
// 3rd, check the Name Servers
return checkNameServers(entry)
.fail(function(entry) {
// None of the above worked, we didn't find any trace of a CDN
return entry;
});
})
});
}
return {
detectCDN: detectCDN
};
// Compares the URL's domain to a list of know CDN, just in case
function checkDomainDirectly(entry) {
var deferred = Q.defer();
var result = checkHostname(url.parse(entry.url).host);
if (result === null) {
deferred.reject(entry);
} else {
entry.weightCheck.cdn = result;
deferred.resolve(entry);
}
return deferred.promise;
}
// Compares a list of HTTP response headers to a matching list
// For example, if there is an X-Akamai-Request-Id header, we know it is Akamai
function checkHeaders(entry) {
var deferred = Q.defer();
var result = cdnDetector.detectFromHeaders(entry.weightCheck.headers);
debug('Headers of %s were compared to the list. Result: %s', entry.url, JSON.stringify(result));
if (result === null) {
deferred.reject(entry);
} else {
entry.weightCheck.cdn = result.cdn;
deferred.resolve(entry);
}
return deferred.promise;
}
function checkNameServers(entry) {
var deferred = Q.defer();
var hostname = url.parse(entry.url).host;
// Send a request to get the Name Servers list
// https://nodejs.org/api/dns.html#dns_dns_resolvens_hostname_callback
dns.resolveNs(hostname, function(error, nameServers) {
if (error) {
debug(error);
debug('Could not find Name Servers for %s', hostname);
deferred.reject(entry);
} else {
debug('Name Servers for %s are ["%s"]', hostname, nameServers.join('", "'));
var cdn = nameServers.map(checkHostname).find(function(entry) {
return entry !== null;
});
if (cdn) {
entry.weightCheck.cdn = cdn;
deferred.resolve(entry);
} else {
deferred.reject(entry);
}
}
});
return deferred.promise;
}
// Compares the hostname to a matching list of hostnames
function checkHostname(hostname) {
var result = cdnDetector.detectFromHostname(hostname);
debug('Hostname %s was compared to the list. Result: %s', hostname, JSON.stringify(result));
return result ? result.cdn : null;
}
// Find an IP address with a DNS lookup
function getIP(hostname) {
var deferred = Q.defer();
// https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback
dns.lookup(hostname, {family: 4}, function(error, address) {
if (error) {
debug('Could not find IP address for %s', hostname);
} else {
debug('The IP address for %s is %s', hostname, address);
}
deferred.resolve(error ? null : address);
});
return deferred.promise;
}
// Compares the IP address agains a list of IP ranges
function checkIPAddress(address) {
var deferred = Q.defer();
var result = cdnDetector.detectFromHeaders(headers);
debug('Headers were compared to the list. Result: %s', JSON.stringify(result));
if (result === null) {
deferred.reject();
} else {
deferred.resolve(result.cdn);
}
return deferred.promise;
}
function saveCDN(cdnName) {
}
};
module.exports = new CDNDetector();
@@ -0,0 +1,71 @@
var http = require('https');
var zlib = require('zlib');
var fs = require('fs');
var CDN_ASN_LIST = {
13335: 'Cloudflare',
15133: 'Verizon',
16625: 'Akamai',
20446: 'StackPath',
20940: 'Akamai',
22822: 'Limelight',
54113: 'Fastly'
};
// Downloads a file and ungzip ip directly
function getGzipped(url, callback) {
var buffer = [];
http.get(url, function(res) {
var gunzip = zlib.createGunzip();
res.pipe(gunzip);
gunzip.on('data', function(data) {
buffer.push(data.toString())
}).on('end', function() {
callback(null, buffer.join(''));
}).on('error', function(e) {
callback(e);
})
}).on('error', function(e) {
callback(e)
});
}
// File provided here: https://iptoasn.com/
console.log('Start downloading the file...');
getGzipped('https://iptoasn.com/data/ip2asn-v4.tsv.gz', function(err, data) {
if (err) {
console.log(err);
return;
}
console.log('Download complete, let\'s start parsing...');
parseFile(data);
});
// Parse the CSV formated file and grag only the interesting IP ranges
function parseFile(data) {
var results = [];
var allTextLines = data.split(/\r\n|\n/);
for (var i = 0; i < allTextLines.length; i++) {
var lineData = allTextLines[i].split('\t');
var cdnName = CDN_ASN_LIST[lineData[2]];
if (cdnName) {
// We found an ASN number from our list. Let's save it!
results.push({
rangeStart: lineData[0],
rangeEnd: lineData[1],
cdn: cdnName
});
}
}
console.log('%d IP ranges found', results.length);
var outputFile = __dirname + '/cdn-ip-list.json';
fs.writeFileSync(outputFile, JSON.stringify(results, null, 2));
console.log('File saved here: %s', outputFile);
}
+79 -17
View File
@@ -4,40 +4,65 @@ var isJpg = require('is-jpg');
var isPng = require('is-png');
var isSvg = require('is-svg');
var isGif = require('is-gif');
var isWebp = require('is-webp');
var isWoff = require('is-woff');
var isWoff2 = require('is-woff2');
var isOtf = require('is-otf');
var isTtf = require('is-ttf');
var isEot = require('is-eot');
var isJson = require('is-json');
var ContentTypeChecker = function() {
function checkContentType(entry) {
var deferred = Q.defer();
debug('Entering contentTypeChecker');
// Setting isSomething values:
switch(entry.type) {
case 'html':
entry.isHTML = true;
break;
case 'xml':
entry.isXML = true;
break;
case 'css':
entry.isCSS = true;
break;
case 'js':
entry.isJS = true;
break;
case 'json':
entry.isJSON = true;
break;
case 'image':
entry.isImage = true;
break;
case 'webfont':
entry.isWebFont = true;
break;
case 'video':
entry.isVideo = true;
break;
case 'favicon':
entry.isFavicon = true;
break;
}
// Now let's check for mistakes by analysing body content. It happens more often then we think!
// Ignore very small files as they are generally tracking pixels
if (entry.weightCheck && entry.weightCheck.bodyBuffer && entry.weightCheck.bodySize > 100) {
var foundType;
try {
foundType = findContentType(entry.weightCheck.bodyBuffer);
if (!entry.contentType || entry.contentType === '') {
if (foundType === null) {
debug('ContentType is empty for file %s', entry.url);
} else {
debug('ContentType is empty for file %s. It should be %s.', entry.url, foundType.mimes[0]);
entry.oldContentType = null;
rewriteContentType(entry, foundType);
}
} else {
if (foundType !== null && foundType.mimes.indexOf(entry.contentType) === -1) {
debug('ContentType %s is wrong for %s. It should be %s.', entry.contentType, entry.url, foundType.mimes[0]);
entry.oldContentType = entry.contentType;
rewriteContentType(entry, foundType);
// If it's an image or a font, then rewrite.
if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont' || foundType.type === 'json')) {
if (foundType.type !== entry.type) {
debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type);
}
rewriteContentType(entry, foundType);
}
} catch(err) {
@@ -71,6 +96,10 @@ var ContentTypeChecker = function() {
return contentTypes.gif;
}
if (isWebp(bodyBuffer)) {
return contentTypes.webp;
}
if (isWoff(bodyBuffer)) {
return contentTypes.woff;
}
@@ -91,6 +120,10 @@ var ContentTypeChecker = function() {
return contentTypes.eot;
}
if (isJson(bodyStr)) {
return contentTypes.json;
}
return null;
}
@@ -114,6 +147,7 @@ var ContentTypeChecker = function() {
var contentTypes = {
jpeg: {
type: 'image',
mimes: ['image/jpeg'],
updateFn: function(entry) {
entry.type = 'image';
@@ -121,6 +155,7 @@ var ContentTypeChecker = function() {
}
},
png: {
type: 'image',
mimes: ['image/png'],
updateFn: function(entry) {
entry.type = 'image';
@@ -128,6 +163,7 @@ var ContentTypeChecker = function() {
}
},
svg: {
type: 'image',
mimes: ['image/svg+xml'],
updateFn: function(entry) {
entry.type = 'image';
@@ -136,27 +172,41 @@ var ContentTypeChecker = function() {
}
},
gif: {
type: 'image',
mimes: ['image/gif'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
webp: {
type: 'image',
mimes: ['image/webp'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
woff: {
type: 'webfont',
mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
entry.isWoff = true;
}
},
woff2: {
type: 'webfont',
mimes: ['font/woff2', 'application/x-font-woff2', 'application/font-woff2'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
entry.isWoff2 = true;
}
},
otf: {
type: 'webfont',
mimes: ['application/x-font-otf', 'font/otf', 'font/opentype', 'application/x-font-opentype'],
updateFn: function(entry) {
entry.type = 'webfont';
@@ -164,19 +214,31 @@ var ContentTypeChecker = function() {
}
},
ttf: {
type: 'webfont',
mimes: ['application/x-font-ttf', 'font/ttf', 'application/x-font-truetype'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
entry.isTTF = true;
}
},
eot: {
type: 'webfont',
mimes: ['application/vnd.ms-fontobject', 'font/eot'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
}
},
json: {
type: 'json',
mimes: ['application/json'],
updateFn: function(entry) {
entry.type = 'json';
entry.isJSON = true;
}
},
};
return {
+28 -126
View File
@@ -3,7 +3,7 @@ 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 htmlMinifier = require('html-minifier');
var FileMinifier = function() {
@@ -141,123 +141,19 @@ var FileMinifier = function() {
// Uglify
function minifyJs(body) {
// Splitting the Uglify function because it sometime takes too long (more than 10 seconds)
// I hope that, by splitting, it can be a little more asynchronous, so the application doesn't freeze.
var deferred = Q.defer();
var startTime = Date.now();
return splittedUglifyStep1(body)
.delay(1)
.then(splittedUglifyStep2)
.delay(1)
.then(function(ast) {
var result = UglifyJS.minify(body, {
// Only do the compression step for smaller files
// otherwise it can take a very long time compared to the gain
if (body.length < 200*1024) {
return splittedUglifyStep3(ast);
} else {
debug('Skipping step 3 because the file is too big (%d bytes)!', body.length);
return ast;
}
})
.delay(1)
.then(splittedUglifyStep4)
.delay(1)
.then(splittedUglifyStep5)
.delay(1)
.then(splittedUglifyStep6)
.delay(1)
.then(splittedUglifyStep7);
}
function splittedUglifyStep1(code) {
var deferred = Q.defer();
var startTime = Date.now();
try {
var toplevel_ast = UglifyJS.parse(code);
var endTime = Date.now();
debug('Uglify step 1 took %dms', endTime - startTime);
deferred.resolve(toplevel_ast);
} catch(err) {
debug('JS syntax error, Uglify\'s parser failed (step 1)');
deferred.reject(err);
}
return deferred.promise;
}
function splittedUglifyStep2(toplevel) {
var deferred = Q.defer();
var startTime = Date.now();
toplevel.figure_out_scope();
compress: (body.length < 200*1024)
});
var endTime = Date.now();
debug('Uglify step 2 took %dms', endTime - startTime);
deferred.resolve(toplevel);
return deferred.promise;
}
debug('Uglify took %dms', endTime - startTime);
deferred.resolve(result.code);
function splittedUglifyStep3(toplevel) {
var deferred = Q.defer();
var startTime = Date.now();
var compressor = UglifyJS.Compressor({warnings: false});
var compressed_ast = toplevel.transform(compressor);
var endTime = Date.now();
debug('Uglify step 3 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep4(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.figure_out_scope();
var endTime = Date.now();
debug('Uglify step 4 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep5(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.compute_char_frequency();
var endTime = Date.now();
debug('Uglify step 5 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep6(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.mangle_names();
var endTime = Date.now();
debug('Uglify step 6 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep7(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
var code = compressed_ast.print_to_string();
var endTime = Date.now();
debug('Uglify step 7 took %dms', endTime - startTime);
deferred.resolve(code);
return deferred.promise;
}
@@ -279,19 +175,25 @@ var FileMinifier = function() {
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);
}
});
try {
var result = htmlMinifier.minify(body, {
collapseWhitespace: true,
conservativeCollapse: true,
continueOnParseError: true,
decodeEntities: true,
minifyCSS: true,
minifyJS: true,
preserveLineBreaks: true,
removeAttributeQuotes: true,
removeComments: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true
});
deferred.resolve(result);
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
@@ -322,7 +224,7 @@ var FileMinifier = function() {
return result;
}
// Avoid losing some trying to compress JS files if they already look minified
// Avoid losing time trying to compress JS files if they already look minified
// by counting the number of lines compared to the total size.
// Less than 2KB per line is suspicious
function looksAlreadyMinified(code) {
+90 -8
View File
@@ -2,6 +2,8 @@ var debug = require('debug')('ylt:fontAnalyzer');
var Q = require('q');
var fontkit = require('fontkit');
var woffTools = require('woff-tools');
var ttf2woff2 = require('ttf2woff2');
var FontAnalyzer = function() {
@@ -14,30 +16,110 @@ var FontAnalyzer = function() {
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
if (entry.isWebFont) {
debug('File %s is a font. Let\'s have a look inside!', entry.url);
getMetricsFromFont(entry, charsListOnPage)
convertToWoff2(entry)
.then(function(entry) {
return getMetricsFromFont(entry, charsListOnPage);
})
.then(function(fontMetrics) {
entry.fontMetrics = fontMetrics;
deferred.resolve(entry);
})
.fail(function(error) {
debug('Could not open the font: %s', error);
deferred.resolve(entry);
});
} else {
deferred.resolve(entry);
}
deferred.resolve(entry);
return deferred.promise;
}
function convertToWoff2(entry) {
var deferred = Q.defer();
debug('Entering font format converter...');
var fileSize = entry.weightCheck.bodySize;
var ttf;
var woff2;
var newFileSize;
if (entry.isWoff2) {
debug('File is already a woff2.');
deferred.resolve(entry);
} else if (fileSize > 1024 * 1024) {
// Don't try to convert huge font files, it would block the server for several minutes
debug('Font is too big, skipping conversion.');
deferred.resolve(entry);
} else if (entry.isWoff) {
debug('File is a woff. Let\'s convert to woff2');
try {
debug('Current file size is %d', fileSize);
ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer);
woff2 = ttf2woff2(ttf);
newFileSize = woff2.length;
debug('New font size is %d', newFileSize);
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
entry.weightCheck.sizeAsWoff2 = newFileSize;
deferred.resolve(entry);
} catch(error) {
deferred.reject(error);
}
} else if (entry.isTTF) {
debug('File is a TTF. Let\'s convert to woff2');
try {
debug('Current file size is %d', fileSize);
woff2 = ttf2woff2(entry.weightCheck.bodyBuffer);
newFileSize = woff2.length;
debug('New image size is %d', newFileSize);
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
entry.weightCheck.sizeAsWoff2 = newFileSize;
deferred.resolve(entry);
} catch(error) {
deferred.reject(error);
}
} else {
// Other font formats are not handled
deferred.resolve(entry);
}
return deferred.promise;
}
// 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));
}
function getMetricsFromFont(entry, charsListOnPage) {
var deferred = Q.defer();
@@ -49,7 +131,7 @@ var FontAnalyzer = function() {
name: font.fullName || font.postscriptName || font.familyName,
numGlyphs: font.numGlyphs,
averageGlyphComplexity: getAverageGlyphComplexity(font),
compressedWeight: entry.weightCheck.afterCompression || entry.weightCheck.bodySize,
compressedWeight: entry.weightCheck.afterGzipCompression || entry.weightCheck.bodySize,
unicodeRanges: readUnicodeRanges(font.characterSet, charsListOnPage),
numGlyphsInCommonWithPageContent: countPossiblyUsedGlyphs(getCharacterSetAsString(font.characterSet), charsListOnPage)
};
+9 -8
View File
@@ -6,7 +6,7 @@ var zlib = require('zlib');
var GzipCompressor = function() {
function compressFile(entry) {
debug('Entering compressFile');
debug('Entering gzip compressor');
return gzipUncompressedFile(entry)
.then(gzipOptimizedFile);
@@ -16,7 +16,7 @@ var GzipCompressor = function() {
function gzipUncompressedFile(entry) {
var deferred = Q.defer();
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.bodyBuffer) {
if (entryTypeCanBeCompressed(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.bodyBuffer) {
debug('Compression missing, trying to gzip file %s', entry.url);
var uncompressedSize = entry.weightCheck.uncompressedSize;
@@ -33,7 +33,7 @@ var GzipCompressor = function() {
if (gainIsEnough(uncompressedSize, compressedSize)) {
debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterCompression = compressedSize;
entry.weightCheck.afterGzipCompression = compressedSize;
} else {
debug('Gzip gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
}
@@ -53,12 +53,12 @@ var GzipCompressor = function() {
function gzipOptimizedFile(entry) {
var deferred = Q.defer();
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
if (entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
debug('Trying to gzip file after minification: %s', entry.url);
var uncompressedSize = entry.weightCheck.optimized;
zlib.gzip(new Buffer(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
zlib.gzip(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
if (err) {
debug('Could not compress minified file with gzip');
debug(err);
@@ -68,7 +68,7 @@ var GzipCompressor = function() {
var compressedSize = buffer.length;
debug('Correctly gziped the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterOptimizationAndCompression = compressedSize;
entry.weightCheck.afterOptimizationAndGzipCompression = compressedSize;
deferred.resolve(entry);
}
@@ -89,13 +89,14 @@ var GzipCompressor = function() {
return (gain > 2048 || (ratio > 0.2 && gain > 100));
}
function entryTypeCanBeGzipped(entry) {
function entryTypeCanBeCompressed(entry) {
return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
}
return {
compressFile: compressFile,
entryTypeCanBeGzipped: entryTypeCanBeGzipped
gainIsEnough: gainIsEnough,
entryTypeCanBeCompressed: entryTypeCanBeCompressed
};
};

Some files were not shown because too many files have changed in this diff Show More