Created
April 17, 2020 17:24
-
-
Save victor-homyakov/bbcefc63de9d85f5e738f859430f6e4c to your computer and use it in GitHub Desktop.
Проверка на масштабирование изображений в браузере
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-var,no-console */ | |
/** | |
* Проверка на масштабирование изображений в браузере. | |
* Срабатывает, если натуральный размер изображения намного больше отображаемого на странице, | |
* то есть браузер грузит большую картинку и масштабирует её до маленькой. | |
*/ | |
(function() { | |
if (!window.Promise || !String.prototype.startsWith || window.MSInputMethodContext) { | |
// Не запускаем проверку в IE11 и браузерах, не поддерживающих нужные API | |
return; | |
} | |
/** | |
* Минимальный дополнительный трафик с картинки, при превышении которого срабатывает проверка | |
* @type {number} | |
*/ | |
var EXTRA_TRAFFIC_KB_LIMIT = 4; | |
class Img { | |
constructor(imgElement, src) { | |
// console.log('Проверяем', imgElement, src); | |
this.img = imgElement; | |
this.src = src || this.img.src; | |
this.className = this.img.className; | |
} | |
/** | |
* Массив URL, которые не будут проверяться | |
*/ | |
ignoredImageSources = [ | |
// 'https://example.com/img/placeholder.png' | |
]; | |
/** | |
* Изображения, которые надо игнорировать: | |
* - или проверено и ничего плохого не найдено | |
* - или уже заведена задача на починку | |
* | |
* Игнорируются, если все перечисленные свойства совпадут. | |
* Если какое-то свойство здесь не указано - оно не будет проверяться. | |
* | |
* Свойства: | |
* className - точный className DOM-элемента | |
* w, h - ширина и высота DOM-элемента | |
* nw, nh - натуральные размеры изображения | |
* xScale, yScale - коэффициент масштабирования по осям | |
*/ | |
ignoredImages = [ | |
]; | |
calculateDimensions() { | |
this.w = this.img.offsetWidth; | |
this.h = this.img.offsetHeight; | |
this.nw = this.img.naturalWidth; | |
this.nh = this.img.naturalHeight; | |
this.calculateScale(); | |
return Promise.resolve(); | |
} | |
calculateScale() { | |
this.xScale = this.nw / this.w; | |
this.yScale = this.nh / this.h; | |
} | |
checkDimensions() { | |
if (this.shouldIgnoreImageBefore()) { | |
return; | |
} | |
this.calculateDimensions().then(function() { | |
if (this.shouldIgnoreImageAfter()) { | |
return; | |
} | |
var w = this.w, | |
h = this.h, | |
nw = this.nw, | |
nh = this.nh; | |
if (w === 0 || h === 0) { | |
// Скрытое изображение - не репортим | |
// this.report('Скрытое изображение, можно грузить лениво'); | |
return; | |
} | |
if (nw <= w && nh <= h) { | |
// Увеличенное изображение - не репортим | |
return; | |
} | |
if (this.xScale === 2 && this.yScale === 2 && this.src.endsWith('_2x')) { | |
// Изображение retina 2x | |
return; | |
} | |
if (this.xScale < 3 && this.xScale > 1 / 3 && this.yScale < 3 && this.yScale > 1 / 3) { | |
// Увеличение или уменьшение менее, чем в 3 раза - OK | |
return; | |
} | |
// 10000 - эмпирическая константа, дающая примерно похожие числа в проверенных случаях | |
// Для более точных результатов надо усложнять алгоритм, что сейчас нецелесообразно, | |
// т.к. самые значительные различия находятся и таким алгоритмом | |
var extraTrafficKb = Math.round((nw * nh - w * h) / 10000); | |
if (extraTrafficKb < EXTRA_TRAFFIC_KB_LIMIT) { | |
return; | |
} | |
this.report( | |
'Масштабированное изображение: потеря трафика около ' + extraTrafficKb + 'кБ' + | |
' видимый размер: ' + w + 'x' + h | |
); | |
}.bind(this)); | |
} | |
shouldIgnoreImageBefore() { | |
return this.ignoredImageSources.indexOf(this.src) !== -1; | |
} | |
matches(props) { | |
for (var prop in props) { | |
if (props.hasOwnProperty(prop) && props[prop] !== this[prop]) { | |
return false; | |
} | |
} | |
return true; | |
} | |
shouldIgnoreImageAfter() { | |
return this.ignoredImages.some(function(props) { | |
return this.matches(props); | |
}, this); | |
} | |
report(message) { | |
message += ' натуральный размер: ' + this.nw + 'x' + this.nh; | |
message += ' class: "' + this.className + '"'; | |
if (!this.src.startsWith('data:image')) { | |
message += ' src: ' + this.src; | |
} | |
console.log(message, this.img); | |
this.img.style.outline = '3px dotted red'; | |
} | |
} | |
class BgImg extends Img { | |
calculateDimensions() { | |
return Promise.all([ | |
this.calculateImgDimensions(), | |
this.calculateBgDimensions() | |
]).then(function() { | |
this.calculateScale(); | |
}.bind(this)); | |
} | |
calculateImgDimensions() { | |
return new Promise(function(resolve) { | |
var img = new Image(); | |
img.onload = function() { | |
img.onload = img.onerror = null; | |
this.nw = img.naturalWidth; | |
this.nh = img.naturalHeight; | |
resolve(); | |
}.bind(this); | |
img.onerror = function() { | |
// Игнорируем ошибку загрузки изображения | |
img.onload = img.onerror = null; | |
this.nw = this.nh = 0; | |
resolve(); | |
}.bind(this); | |
img.src = this.src; | |
}.bind(this)); | |
} | |
calculateBgDimensions() { | |
var backgroundSize = this.img.style.backgroundSize; | |
if (backgroundSize) { | |
var match = backgroundSize.match(/(\d+)px (\d+)px/); | |
if (match) { | |
this.w = parseInt(match[1]); | |
this.h = parseInt(match[2]); | |
return; | |
} | |
} | |
this.w = this.img.offsetWidth || 0; | |
this.h = this.img.offsetHeight || 0; | |
} | |
shouldIgnoreImageBefore() { | |
var src = this.src; | |
if ( | |
src === 'none' || | |
src.startsWith('https://favicon.yandex.net/favicon/v2/') || | |
this.ignoredImageSources.indexOf(src) !== -1 | |
) { | |
return true; | |
} | |
if (src.startsWith('data:image/')) { | |
// Короткие data-url не проверяем | |
return src.length < 1000; | |
} | |
return !/^(https?:\/\/|\/\/)/.test(src); | |
} | |
} | |
var i; | |
var images = document.querySelectorAll('img[src]'); | |
console.log('Проверяю', images.length, 'изображений'); | |
for (i = 0; i < images.length; i++) { | |
new Img(images[i]).checkDimensions(); | |
} | |
/* | |
background-image только в inline-стилях можно найти так: | |
document.querySelectorAll('[style*="background"][style*="url("]') | |
Но нас интересуют computed-стили, поэтому проверяем все элементы DOM | |
*/ | |
var allElements = document.querySelectorAll('*'); | |
var bgImagesCount = 0; | |
for (i = 0; i < allElements.length; i++) { | |
var container = allElements[i]; | |
var backgroundImage = getComputedStyle(container).backgroundImage; | |
if (!backgroundImage.startsWith('url(')) { | |
continue; | |
} | |
backgroundImage = backgroundImage.replace(/^url\("?|"?\)$/g, ''); | |
if (backgroundImage.indexOf('url(') === -1) { | |
new BgImg(container, backgroundImage).checkDimensions(); | |
bgImagesCount++; | |
continue; | |
} | |
var bgImages = backgroundImage.split(/"?\),\s*url\("?/); | |
bgImagesCount += bgImages.length; | |
for (var j = 0; j < bgImages.length; j++) { | |
new BgImg(container, bgImages[j]).checkDimensions(); | |
} | |
} | |
console.log('Проверяю', bgImagesCount, 'фоновых изображений'); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment