|
@@ -18,6 +18,7 @@ var imageOptimizer = require('./imageOptimizer');
|
|
var fileMinifier = require('./fileMinifier');
|
|
var fileMinifier = require('./fileMinifier');
|
|
var gzipCompressor = require('./gzipCompressor');
|
|
var gzipCompressor = require('./gzipCompressor');
|
|
var contentTypeChecker = require('./contentTypeChecker');
|
|
var contentTypeChecker = require('./contentTypeChecker');
|
|
|
|
+var fontAnalyzer = require('./fontAnalyzer');
|
|
|
|
|
|
|
|
|
|
var Redownload = function() {
|
|
var Redownload = function() {
|
|
@@ -44,6 +45,12 @@ var Redownload = function() {
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ // Prevent a bug with the font analyzer on empty pages
|
|
|
|
+ var differentCharacters = '';
|
|
|
|
+ if (data.toolsResults.phantomas.offenders.differentCharacters && data.toolsResults.phantomas.offenders.differentCharacters.length > 0) {
|
|
|
|
+ differentCharacters = data.toolsResults.phantomas.offenders.differentCharacters[0];
|
|
|
|
+ }
|
|
|
|
+
|
|
// Transform every request into a download function with a callback when done
|
|
// Transform every request into a download function with a callback when done
|
|
var redownloadList = requestsList.map(function(entry) {
|
|
var redownloadList = requestsList.map(function(entry) {
|
|
return function(callback) {
|
|
return function(callback) {
|
|
@@ -58,8 +65,12 @@ var Redownload = function() {
|
|
|
|
|
|
.then(gzipCompressor.compressFile)
|
|
.then(gzipCompressor.compressFile)
|
|
|
|
|
|
|
|
+ .then(function(entry) {
|
|
|
|
+ return fontAnalyzer.analyzeFont(entry, differentCharacters);
|
|
|
|
+ })
|
|
|
|
+
|
|
.then(function(newEntry) {
|
|
.then(function(newEntry) {
|
|
- debug('File %s - Redownloaded, optimized, minified, compressed: done', entry.url);
|
|
|
|
|
|
+ debug('File %s - Redownloaded, optimized, minified, compressed, analyzed: done', entry.url);
|
|
callback(null, newEntry);
|
|
callback(null, newEntry);
|
|
})
|
|
})
|
|
|
|
|
|
@@ -134,6 +145,18 @@ var Redownload = function() {
|
|
offenders.identicalFiles = listIdenticalFiles(results);
|
|
offenders.identicalFiles = listIdenticalFiles(results);
|
|
metrics.identicalFiles = offenders.identicalFiles.avoidableRequests;
|
|
metrics.identicalFiles = offenders.identicalFiles.avoidableRequests;
|
|
|
|
|
|
|
|
+ // Fonts count
|
|
|
|
+ offenders.fontsCount = listFonts(results);
|
|
|
|
+ metrics.fontsCount = offenders.fontsCount.count;
|
|
|
|
+
|
|
|
|
+ // Heavy fonts
|
|
|
|
+ offenders.heavyFonts = listHeavyFonts(results);
|
|
|
|
+ metrics.heavyFonts = offenders.heavyFonts.totalGain;
|
|
|
|
+
|
|
|
|
+ // Unused Unicode ranges
|
|
|
|
+ offenders.unusedUnicodeRanges = listUnusedUnicodeRanges(results);
|
|
|
|
+ metrics.unusedUnicodeRanges = offenders.unusedUnicodeRanges.count;
|
|
|
|
+
|
|
|
|
|
|
data.toolsResults.redownload = {
|
|
data.toolsResults.redownload = {
|
|
metrics: metrics,
|
|
metrics: metrics,
|
|
@@ -381,7 +404,7 @@ var Redownload = function() {
|
|
var avoidableRequestsCount = 0;
|
|
var avoidableRequestsCount = 0;
|
|
|
|
|
|
requests.forEach(function(req) {
|
|
requests.forEach(function(req) {
|
|
- var requestHash = md5(req.weightCheck.body);
|
|
|
|
|
|
+ var requestHash = md5(req.weightCheck.bodyBuffer);
|
|
|
|
|
|
// Try to exclude tracking pixels
|
|
// Try to exclude tracking pixels
|
|
if (req.weightCheck.bodySize < 80 && req.type === 'image') {
|
|
if (req.weightCheck.bodySize < 80 && req.type === 'image') {
|
|
@@ -414,6 +437,126 @@ var Redownload = function() {
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ function listFonts(requests) {
|
|
|
|
+ var list = [];
|
|
|
|
+
|
|
|
|
+ requests.forEach(function(req) {
|
|
|
|
+ if (req.isWebFont) {
|
|
|
|
+ list.push({
|
|
|
|
+ url: req.url,
|
|
|
|
+ size: req.weightCheck.bodySize
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ count: list.length,
|
|
|
|
+ list: list
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function listHeavyFonts(requests) {
|
|
|
|
+ var list = [];
|
|
|
|
+ var totalGain = 0;
|
|
|
|
+ var heavyFontsCount = 0;
|
|
|
|
+ var MAX_FONT_WEIGHT = 40 * 1024;
|
|
|
|
+
|
|
|
|
+ requests.forEach(function(req) {
|
|
|
|
+ if (req.isWebFont && req.fontMetrics) {
|
|
|
|
+ list.push({
|
|
|
|
+ url: req.url,
|
|
|
|
+ weight: req.weightCheck.bodySize,
|
|
|
|
+ numGlyphs: req.fontMetrics.numGlyphs,
|
|
|
|
+ averageGlyphComplexity: req.fontMetrics.averageGlyphComplexity
|
|
|
|
+ });
|
|
|
|
+ if (req.weightCheck.bodySize > MAX_FONT_WEIGHT) {
|
|
|
|
+ totalGain += req.weightCheck.bodySize - MAX_FONT_WEIGHT;
|
|
|
|
+ heavyFontsCount ++;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ count: heavyFontsCount,
|
|
|
|
+ fonts: list,
|
|
|
|
+ totalGain: totalGain
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function listUnusedUnicodeRanges(requests) {
|
|
|
|
+ var list = [];
|
|
|
|
+ var unusedUnicodeRanges = 0;
|
|
|
|
+
|
|
|
|
+ requests.forEach(function(req) {
|
|
|
|
+ if (req.isWebFont && req.fontMetrics && req.fontMetrics.unicodeRanges) {
|
|
|
|
+ var ranges = [];
|
|
|
|
+ var others = null;
|
|
|
|
+ var rangeNames = Object.keys(req.fontMetrics.unicodeRanges);
|
|
|
|
+
|
|
|
|
+ rangeNames.forEach(function(rangeName) {
|
|
|
|
+ var range = req.fontMetrics.unicodeRanges[rangeName];
|
|
|
|
+
|
|
|
|
+ // Exclude "Others"
|
|
|
|
+ if (rangeName === 'Others') {
|
|
|
|
+ if (range.numGlyphsInCommonWithPageContent === 0 && range.charset.length > 50) {
|
|
|
|
+ range.underused = true;
|
|
|
|
+ unusedUnicodeRanges ++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ others = range;
|
|
|
|
+ } else if (range.charset.length > 0) {
|
|
|
|
+ // Now lets detect if the current Unicode range is unused.
|
|
|
|
+ // Reminder: range.coverage = glyphs declared in this range, divided by the range size
|
|
|
|
+ if (range.coverage > 0.25 && range.numGlyphsInCommonWithPageContent === 0) {
|
|
|
|
+ range.underused = true;
|
|
|
|
+ unusedUnicodeRanges ++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ ranges.push(range);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ // Detect if it's a icons font : if more than 90% of the icons are
|
|
|
|
+ // in the "Others", it looks like one.
|
|
|
|
+ if (others && others.charset.length / req.fontMetrics.numGlyphs > 0.9) {
|
|
|
|
+
|
|
|
|
+ list.push({
|
|
|
|
+ url: req.url,
|
|
|
|
+ weight: req.weightCheck.bodySize,
|
|
|
|
+ isIconFont: true,
|
|
|
|
+ glyphs: req.fontMetrics.numGlyphs,
|
|
|
|
+ numGlyphsInCommonWithPageContent: req.fontMetrics.numGlyphsInCommonWithPageContent
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // And if less than 5% of the icons are used, let's report it as underused
|
|
|
|
+ if (others && others.numGlyphsInCommonWithPageContent / others.charset.length <= 0.05) {
|
|
|
|
+ unusedUnicodeRanges ++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Not an icons font
|
|
|
|
+ } else {
|
|
|
|
+ if (others) {
|
|
|
|
+ // Insert back "Others" at the end of the list
|
|
|
|
+ ranges.push(others);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ list.push({
|
|
|
|
+ url: req.url,
|
|
|
|
+ weight: req.weightCheck.bodySize,
|
|
|
|
+ isIconFont: false,
|
|
|
|
+ unicodeRanges: ranges
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ count: unusedUnicodeRanges,
|
|
|
|
+ fonts: list
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
|
|
function redownloadEntry(entry, httpAuth) {
|
|
function redownloadEntry(entry, httpAuth) {
|
|
var deferred = Q.defer();
|
|
var deferred = Q.defer();
|
|
@@ -521,7 +664,7 @@ var Redownload = function() {
|
|
|
|
|
|
var uncompressedSize = 0; // size after uncompression
|
|
var uncompressedSize = 0; // size after uncompression
|
|
var bodySize = 0; // bytes size over the wire
|
|
var bodySize = 0; // bytes size over the wire
|
|
- var body = ''; // plain text body (after uncompressing gzip/deflate)
|
|
|
|
|
|
+ var bodyChunks = []; // an array of buffers
|
|
var isCompressed = false;
|
|
var isCompressed = false;
|
|
|
|
|
|
function tally() {
|
|
function tally() {
|
|
@@ -531,8 +674,10 @@ var Redownload = function() {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ var body = Buffer.concat(bodyChunks);
|
|
|
|
+
|
|
var result = {
|
|
var result = {
|
|
- body: body,
|
|
|
|
|
|
+ bodyBuffer: body,
|
|
headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
|
|
headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
|
|
bodySize: bodySize,
|
|
bodySize: bodySize,
|
|
isCompressed: isCompressed,
|
|
isCompressed: isCompressed,
|
|
@@ -548,7 +693,8 @@ var Redownload = function() {
|
|
var gzip = zlib.createGunzip();
|
|
var gzip = zlib.createGunzip();
|
|
|
|
|
|
gzip.on('data', function (data) {
|
|
gzip.on('data', function (data) {
|
|
- body += data;
|
|
|
|
|
|
+
|
|
|
|
+ bodyChunks.push(data);
|
|
uncompressedSize += data.length;
|
|
uncompressedSize += data.length;
|
|
}).on('end', function () {
|
|
}).on('end', function () {
|
|
isCompressed = true;
|
|
isCompressed = true;
|
|
@@ -570,7 +716,7 @@ var Redownload = function() {
|
|
var deflate = zlib.createInflate();
|
|
var deflate = zlib.createInflate();
|
|
|
|
|
|
deflate.on('data', function (data) {
|
|
deflate.on('data', function (data) {
|
|
- body += data;
|
|
|
|
|
|
+ bodyChunks.push(data);
|
|
uncompressedSize += data.length;
|
|
uncompressedSize += data.length;
|
|
}).on('end', function () {
|
|
}).on('end', function () {
|
|
isCompressed = true;
|
|
isCompressed = true;
|
|
@@ -587,12 +733,8 @@ var Redownload = function() {
|
|
|
|
|
|
break;
|
|
break;
|
|
default:
|
|
default:
|
|
- if (contentType === 'image/jpeg' || contentType === 'image/png') {
|
|
|
|
- res.setEncoding('binary');
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
res.on('data', function (data) {
|
|
res.on('data', function (data) {
|
|
- body += data;
|
|
|
|
|
|
+ bodyChunks.push(data);
|
|
uncompressedSize += data.length;
|
|
uncompressedSize += data.length;
|
|
bodySize += data.length;
|
|
bodySize += data.length;
|
|
}).on('end', function () {
|
|
}).on('end', function () {
|