Skip to content

Instantly share code, notes, and snippets.

@victor-homyakov
Created April 17, 2020 17:24
Show Gist options
  • Save victor-homyakov/bbcefc63de9d85f5e738f859430f6e4c to your computer and use it in GitHub Desktop.
Save victor-homyakov/bbcefc63de9d85f5e738f859430f6e4c to your computer and use it in GitHub Desktop.
Проверка на масштабирование изображений в браузере
/* 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