Второй. Кеширование. Эффективное кеширование всего и вся в JavaScript

Воскресенье, 7 февраля 2010 года, 12:52. Напечатано в категориях JavaScript, Кодекс, Оптимизация20

Здравствуйте дорогие друзья ;)

Прошло время с тех пор, как была опубликована первая статья цикла об оптимизации. Перед началом её публикации я поймал себя на мысли “Когда будет к ней написано хоть 10 комментариев, так и продолжу!”. Почему мне нужны комментарии? Я просто хочу удостоверится, что эта статья(да и будущий цикл) будет кому-то нужна!
Так как вы читаете сейчас этот текст – это значит, что на первой статье уже есть 10 комментариев(на момент написания их там 11).
Там я немного сжульничал и посчитал свои комментарии вместе со всеми.
В этой статье я так жульничать не буду, и проведу новый эксперимент – когда к статье “Второй. Кеширование. Эффективное кеширование всего и вся в JavaScript” опубликуют комментарии 10 разных человек – я продолжу цикл :)

Итак, обратимся к wiki:

Кэш или кеш (англ. cache, от фр. cacher — прятать) — промежуточный буфер с быстрым доступом, содержащий копию той информации, которая хранится в оперативной памяти с менее быстрым доступом, но с наибольшей вероятностью может быть оттуда запрошена. Доступ к данным в кэше идёт быстрее, чем выборка исходных данных из медленной памяти или их перевычисление, за счёт чего уменьшается среднее время доступа.

В JavaScript, чтобы что-то закешировать надо это что-то назначить на кое-что другое, которое будет возвращать это что-то намного быстрее.

Рассмотрим обычный код:

// Создаём объект
var obj = {
  number: 88,
  print: function( a ){
    alert( 'В функцию передали число ' + a );
  }
};

// Запускаем функцию
obj.print( 555 );

При запуске функции print интерпретатор JavaScript производит довольно объёмные операции:

  1. Ищет объект obj и обращается к нему
  2. Запрашивает у объекта obj список его свойств
  3. Ищет свойство print в общем списке свойств объекта
  4. Если находит, то возвращает это свойство
  5. Выполняет функцию, которая возвращена данным свойством

Теперь по наглядней. Первый пункт:

// обращение к объекту
obj

Второй, третий и четвёртый пункт:

obj.print

Пятый пункт:

obj.print( 555 )

Согласитесь, здесь идёт довольно обширный список действий! Было бы намного проще, если бы мы запустили сразу эту функцию без лишних обращений!
В этом нам и поможет феномен кеширования :)

var
  // Создаём объект
  obj = {
    number: 88,
    print: function( a ){
      alert( 'В функцию передали число ' + a );
    }
  },
  // Создаём ссылку на нужную функцию
  fn = obj.print;

// Запускаем функцию
fn( 555 );

Здесь мы переменной fn просто присвоили ссылку на функцию obj.print
fn = obj.print; – данный код выполняет пункты с первого по четвёртый.

Интерпретатор в следующем коде выполняет данные действия:

  1. Запрашивает переменную fn и возвращает прямую ссылку на функцию obj.print
  2. Выполняет функцию
fn( 555 );

Конечно такой код пока ничего нам не даёт по сравнению с первым листингом и мы пока не выигрываем по скорости.
Но, вот уже существенный пример оптимизации кода:
Начальный код:

// Создаём объект
var obj = {
  number: 88,
  print: function( a ){
    alert( 'В функцию передали число ' + a );
  }
};

// Запускаем функцию
obj.print( 555 );
// Запускаем функцию ещё раз
obj.print( 842 );

Оптимизированный вариант:

var
  // Создаём объект
  obj = {
    number: 88,
    print: function( a ){
      alert( 'В функцию передали число ' + a );
    }
  },
  // Создаём ссылку на нужную функцию
  fn = obj.print;

// Запускаем функцию
fn( 555 );
// Запускаем функцию ещё раз
fn( 842 );

Таким образом мы сократили первоначальный код минимум на 4 пункта первоначальных действий. Если было бы 3 вызова функции fn, то мы сократили бы список действий на 8 пунктов и т. д.

А отсюда и вытекает 4 правило оптимизации :)

Правило 4
Если вы используете какой-либо элемент JavaScript более одного раза – обязательно закешируйте его!

Если у вас выработается такая привычка в дальнейшем – она будет очень помогать вам ;)
Примечание. Конечно у этого правила есть разумные рамки, и например не нужно кешировать простые переменные в теле одной функции

function lol(){
  var t = function(){ /* ...code... */ };
  // Псевдо кеширование
  var c = t;
}

Реальные примеры кеширования

Из первой статьи мы уже знаем, как идёт обращение внутри функций к переменным и объектам и знаем, что порой это весьма затратно!

function lol(){
  window.func1();
  alert( document.cookie );
  location.hach = 'bu';
}

Данный код затратен в плане производительности и его можно намного улучшить!

window.func1();

Здесь функция ищет переменную window в предыдущих “функциях-обёртках”. Если она её не находит, уже идёт обращение к глобальному объекту window.
Это затратно! Давайте ускорим это дело:

function lol(){
  var window = this;
  window.func1();
  alert( document.cookie );
  location.hach = 'bu';
}

И всё! Мы сохранили глобальный объект window в переменную window. Дальнейшие обращения к window внутри нашей функции будет довольно быстры!

Объекты document и location являются свойствами объекта window. И В нашем коде они производят такой же затратный поиск до глобального объекта :(
Если есть затраты, значит уже можно улучшить!

function lol(){
  var window = this;
  window.func1();
  alert( window.document.cookie );
  window.location.hach = 'bu';
}

Есть! Уже быстрей! Улучшим ещё!

function lol(){
  var
    window = this,
    document = this.document,
    location = this.location;
  window.func1();
  alert( document.cookie );
  location.hach = 'bu';
}

Ещё!

function lol(){
  var
    window = this,
    document = window.document,
    location = window.location;
  window.func1();
  alert( document.cookie );
  location.hach = 'bu';
}

И экономим место :)
Самый оптимизированный код при его новом функционале:

function lol(){
  var
    w = this,
    doc = w.document,
    loc = w.location;

  w.func1();
  alert( doc.cookie );
  loc.hach = 'bu';

  w.func1();
  alert( doc.cookie );
  loc.hach = 'bu';

}

Примечание. Код выше мог вам сказать то, что автор – законченный JS-маньяк. Это совсем не так, он только начинающий :)

Всегда кешируйте результат выполнения функции

Предположим, у нас есть такой код:

alert( 'Новое число ' + parseInt( '522' ) );
alert( 'Вам нравится число ' + parseInt( '522' ) + '?' );

Увидев, что нарушается 4-ое правило оптимизации, сразу исправим код:

var n = parseInt( '522' );
alert( 'Новое число ' + n );
alert( 'Вам нравится число ' + n + '?' );

И ещё очень актуальный пример кеширования результата функции при использовании библиотеки jQuery. Предположим, что мы имеем такой код:

$( 'a' )
.click(
  function(){
    $( this )
    .attr( 'class', $( this ).attr( 'class' ) + ' _' + $( this ).hasClass( 'lol' ) );
  }
);

Тогда очень хорошим тоном будет закешировать результат выполнения jQuery функции:

$( 'a' )
.click(
  function(){
    var $this = $( this );
    $this
    .attr( 'class', $this.attr( 'class' ) + ' _' + $this.hasClass( 'lol' ) );
  }
);

Вот и всё :)

И на последок хочу вам пожелать то, чтобы процесс кеширования доставлял вам сплошное удовольствие, а выходной код этого процесса блестал ярче солнца ;)

Спасибо за внимание и удачи ;)
С уважением, Regent :)



Комментарии(20)

Alexi Lorenz пишет в воскресенье, 07.02.2010 в 15:43 Ответить
Спасибо за статью ;)
Прочитал, интересно, прокомментил, заретвитил.
До следующей статьи теперь на один комментарий меньше ;)
Regent пишет в воскресенье, 07.02.2010 в 16:11 Ответить
@Alexi Lorenz, спасибо за ретвит

Пока обдумываю тему следующей статьи
Alexi Lorenz пишет в воскресенье, 07.02.2010 в 16:25 Ответить
Знаешь, ещё было бы неплохо померять время выполнения функции до и после оптимизации. В Хроме, например, можно замерять время выполнения ф-ций.

А в следущей статье напиши про AJAX, популярная ведь технология ;)
Regent пишет в воскресенье, 07.02.2010 в 16:35 Ответить
@Alexi Lorenz, тут даже пока нечего замерять, время получится чудовищно маленьким

А про AJAX тоже ещё рановато писать.
Столько всего до него надо бы сначала расписать
hazzik пишет в воскресенье, 07.02.2010 в 20:44 Ответить
поздравляю, ты только что изобрел рефакторинг
alexa пишет в понедельник, 08.02.2010 в 6:41 Ответить
Как-то не задумывался об кешировании в js. Спасибо!
Ed пишет во вторник, 09.02.2010 в 11:21 Ответить
На самом деле, описан очень важный механизм. Иногда он весьма необходим. Особенно важно при частом вызове функций, например, для анимации, drag&drop и т.п. Обязательно надо применить у себя на движке окон (http://webokna.blogspot.com). Спасибо за информацию.
TRAHOMOTO пишет во вторник, 09.02.2010 в 23:48 Ответить
Проглотил! Давай еще!
Regent пишет в среду, 10.02.2010 в 9:15 Ответить
@TRAHOMOTO, будет скоро, только тему надо придумать
Alexi Lorenz пишет в среду, 10.02.2010 в 14:04 Ответить
А знаешь, чего я подумал? Напиши о разных приёмах оформления интерфейса с помощью jQuery. Я вот у тебя в шапке увидел няшные кнопочки, а в футере светящийся текст. Завидно же, расскажи методы? =)
Да, к оптимизации это не относится, но хотелось бы пост об этом.
Regent пишет в среду, 10.02.2010 в 17:54 Ответить
@Alexi Lorenz, запросто
Только всё перечисленное - это CSS3
Чистяков Денис пишет в четверг, 11.02.2010 в 17:05 Ответить
Спасибо за статью, продолжай писать, читать по прежнему интерсно и это радует.
Вот мне только одно стало интересно, а не быстрее ли все таки вызов свойства / метода у закешированного объекта, чем создание нового кеша и вызов из него.
Ведь создание переменной, тоже требует времени.
Т.е. не быстрее ли:

function lol(){
  var window = this;
  window.func1();
  alert( window.document.cookie );
  window.location.hach = 'bu';
}

чем:

function lol(){
  var
    window = this,
    document = window.document,
    location = window.location;
  window.func1();
  alert( document.cookie );
  location.hach = 'bu';
}
Regent пишет в четверг, 11.02.2010 в 21:31 Ответить
@Чистяков Денис, совершенно правы, в вашем примере первый вариант намного быстрее

Но, если что-то не понятно, нужно сразу обратится правилу
Правило 4
Если вы используете какой-либо элемент JavaScript более одного раза – обязательно закешируйте его!

Т. е. если бы document в каком-то другом примере использовался бы более одного раза, то да, нужно кешировать, как показано выше.

Примерами хотел показать не столько именно код, сколько саму процессию кеширования
Сергей пишет в субботу, 13.02.2010 в 1:45 Ответить
Не все так просто в мире JS Во-первых, по всем правилам оптимизации прибегать к "кешированию" стоит в самых узких местах, выявленных в ходе тестов на производительность. Как говорил Кнут "преждевременная оптимизация - корень всех зол". И во-вторых, используя приведенный в статье подход, вы кое-что не учитываете.

var
  // Создаём объект
  obj = {
    number: 88,
    print: function( a ){
      alert( 'В функцию передали число ' + a );
    }
  },
  // Создаём ссылку на нужную функцию
  fn = obj.print;

// Запускаем функцию
fn( 555 );
// Запускаем функцию ещё раз
fn( 842 );


Использование переменной fn ошибочно! Почему? Потому что не сохраняется контекст выполнения кода. Если первоначально функция print вызывалась в контексте obj, то в последующем - в контексте window. И не дай Бог функция print будет содержать обращение к this - тут-то вас и настигнет наказание В общем, автору стыдно должно быть))
Если уж очень приспичит "кэшировать", то надо это делать как-то так:

var
  // Создаём объект
  obj = {
    msg: 'В функцию передали число ',
    number: 88,
    print: function( a ){
      alert(this.msg  + a );
    }
  },
  // Создаём ссылку на нужную функцию
  fn = obj.print;

// Запускаем функцию
fn.call(obj, 555 );
// Запускаем функцию ещё раз
fn.call(obj, 842 );


Но как видно, это тоже самое, и значит в оптимизации не было смысла, что вообще и требовалось доказать!

P.S. Да, в частных случаях можно использовать короткий вызов, но на практике выигрыша нет никакого. Увы, оно того не стоит...
P.P.S И да, называть это кешированием не стоит. Реальное кеширование - это когда вы что-то просчитали-высчитали, и в следующий раз при тех же самых условиях вы этого делать не будете, а воспользуетесь готовыми данными...

Peace!
Regent пишет в субботу, 13.02.2010 в 8:18 Ответить
@Сергей, спасибо за комментарий.

Статьей хотел показать самые основы широкомысления
Она направлена на начинающих разработчиков, от чего я и отталкивался.

На счёт контекста - да, он не сохраняется.
Тут уже разработчик сам думает своей головой, а надо ли?
В крайнем случае можно использовать jQuery.proxy

А всё таки от показанного мной кеширования польза есть, как не спорьте.
Ну например при вызове 1000-чу раз document в анонимной функции мы сэкономим ресурсов, всего лишь сохранив ссылку на document.

P. S. Пошёл читать ваш блог
Сергей пишет в субботу, 13.02.2010 в 15:07 Ответить
Бесспорно, сэкономите, но это другое правило: все обращения к DOM должны быть сведены к минимуму. Плюс, есть еще одно правило, применимое не только к ссылкам на DOM: доступ к локальным переменным быстрее, нежели к глобальным. Оптимизация JS - большая наука, понимание сполна которой приходит лишь на суровой практике ;)

Насчет P.S. Было бы у меня больше времени - с удовольствием поделился бы своими мыслями об этом у себя в блоге.
Ensiferum пишет в среду, 17.02.2010 в 15:40 Ответить
Прочел, задумался )) Добавил в закладки блог
kaban пишет в четверг, 08.04.2010 в 2:10 Ответить
аа вот что вы имели в иду когда говорили о "Хороший оптимизированный сайт на MODx у мастера будет просто летать!", так? Ну ведь так можно и joomla оптимизировать?
Regent пишет в четверг, 08.04.2010 в 7:16 Ответить
@kaban, Можно, только её часто оптимизируют?
stich6269 пишет во вторник, 01.06.2010 в 4:25 Ответить
ставил однажды плагин для вордпрес, который кешировал всю страницу... остался не очень доволен, так как при доработке шаблона возникали проблемы с просмотром результата, но если все уже как говорится под ключ, то супер, ускоряет роботу существенно
Уведомлять меня о новых комментариях по почте
Для корректной работы отправки комментариев необходимо включить JavaScript
Либо скачать новый нормальный браузер