Прелоад изображений

Автор: Евгений Рыжков Дата публикации: 19.12.2009

Встречаются задачи, когда на веб странице что-то всплывет, выскакивает или еще каким-то образом появляется. Или же другая ситуация — при наведении на элемент он меняет свой вид (например, при наведении на кнопку).

Проблема

Блоки, которые появляются, при загрузке страницы скрыты. Обычно с помощью display: none. Беда в том, что фоновые изображения и картинки в скрытых блоках при загрузке странциы не загружаются, а начинают загружаться при появлении скрытого блока. В итоге пользователь некоторое время наблюдает какое-то недоразумение (смотри видео):

Аналогичная ситуация, если элемент меняет фоновый рисунок при каком-то событии. Например, часто встречается смена вида кнопки (фонового изображения) при наведении — так называемый rollover эффект:

.button {
display: block;
width: 100px;
height: 25px;
background: url(path-to/button.png);
}
.button:hover {
background: url(path-top/button-over.png);
}

При наведении на кнопку у нее меняется фон на другое изображение. Но при этом button-over.png не загрузится вместе со страницей, а начнет загружаться только когда пользователь наведет на кнопку. Пока к серверу дойдет запрос, пока тот ответит, пока загрузится картинка пользователь будет наблюдать кнопку без фона. Будем от этого избавляться.

Решение 1 — подходит для rollover эффекта

И так, при наведении на объект нам нужно сменить ему картинку. Если объект имеет фиксированные размеры (width и height) или хотя бы только высоту, мы можем легко избавиться от второй отдельной картинки, воспользовавшись техникой CSS спрайтов, а это в свою очередь устранит косяк:

фон для кнопки с rollover эффектом
.button {
display: block;
width: 100px;
height: 25px;
background: url(path-to/button-with-rollover.png); /* склеенная картинка для двух состояний кнопки */
}
.button:hover {
background-position: 0 -25px; /* смещаем фон согласно подготовленной картинки */
}

Проверено в:

Все очень просто, но и подходит далеко не для всех случаев, особенно, если вспомнить про проблему со скрытыми блоками.

Поставим себе простую задачу с показом блока по клику на элементе, знаем о проблеме, которая возникнет при показе блока, будем ее героически преодолевать:

что должны получить - показ скрытого блока с загруженным фоном
<div id="showContacts">Показать контакты</div>
<div id="contactsPopUp">
	[...] <!-- опускаю остальные элементы, они к примеру отношения не имеют -->
</div>

CSS:

#contactsPopUp {
background: (path-to/popup-back.png);
display: none;
[...]
}

Решение 2 — javascript

То же все просто — при загрузке страницы javascript подгружает картинки, которые необходимы:

<script type="text/javascript">
<!--
	if(document.images) /* если доступен объект Image */
	{
		var pic1=new Image(); /* создаем объект - изображение. указывем размеры изображения */
		pic1.src="path-to/popup-back.png"; /* путь к изображению */
	}
-->
</script>
<div id="showContacts">Показать контакты</div>
<div id="contactsPopUp">
	[...]
</div>

В боевых условиях нужно вынести скрипт в отдельный файл, чтобы не засорять код. Скрипт прост, но народ пишет, что это не кроссбраузерное решение, т.к., если просто создать объект image, не все бразеры начинают загружать изображение. Чтоб прелоад начался наверняка, небоходимо, чтобы эти картинки находились на странице. Потому есть такое решение:

<script type="text/javascript">
<!--

function init() {
	// quit if this function has already been called
	if (arguments.callee.done) return;
	
	// flag this function so we don't do the same thing twice
	arguments.callee.done = true;

	// preload images
    preload([
		'images/mob.png',
		'images/mail.png',
		'images/phone.png',
		'images/contactsPopupLeft.png',
		'images/contactsPopupLeftToUp.png',
		'images/contactsPopup.png',
		'images/contactsPopupRight.png'
	]);

   };

/* for Mozilla */
if (document.addEventListener)
{
	document.addEventListener("DOMContentLoaded", init, false);
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
	document.write("<script defer src=js/ie_onload.js><"+"/script<");
/*@end @*/

/* for other browsers */
window.onload = init;

function preload(images) {
    if (typeof document.body == "undefined") return;
    try {

        var div = document.createElement("div");
        var s = div.style;
		    s.position = "absolute";
        s.top = s.left = 0;
        s.visibility = "hidden";
        document.body.appendChild(div);
		div.innerHTML = "<img src="" + images.join("" /><img src="") + "" />";
	 }
	 catch(e) {
        // Error. Do nothing.
	}
}

-->

</script>

Что тут происходит? При загрузке документа динамически создатаеся div. В него добвляются img с нужными тебе путями к картинкам. Чтобы div не мешал, он позиционируется абсолютно и скрывается с помощью visibility: hidden (именно через visibility, т.к. при display: none изображения не грузятся).

Почему просто не воспользоваться window.onload, а городить сложную инициализацю? Событие window.onload проиходит до загрузки DOM, а т.к. нам нужно вставить новый узел (div и img), нужно дождаться этой самой загрузки DOM:

Заметки
  • Есть вероятность, что в firefox такой прием не срабоатет, т.к. бывет у него глюк — загруженные картинки не берутся из кеша, а снова грузятся с сервера:
  • Как остледить, что изображение подгузилось? — Хороший способ воспользоваться какой-нибудь программой, которая отслеживает обращения к серверу:
    обращения к серверу в firebug
    Некоторые подходящие для этого плагины:

Теперь рассмотрим недостаток данного метода — это лишние элементы в DOM структуре, что не лучшим образом скажется на скорости работы front-end скриптов. На небольших проектах этот дополнительный элемент особо не на чем не скажется, но вот для крупного портала, на котором будет происходить интенсивная работа с DOM элементами это уже будет влиять, особенно если учесть, что подгруженных картинок может быть несколько десятков.

Попробуем удалить этот дополнительный узел как он закончит свою миссию — подгрузит все картинки (полностью загрузится последняя):

<script type="text/javascript">
<!--

function init() {
	// quit if this function has already been called
	if (arguments.callee.done) return;
	
	// flag this function so we don't do the same thing twice
	arguments.callee.done = true;

	// preload images
    preload([
		'images/mob.png',
		'images/mail.png',
		'images/phone.png',
		'images/contactsPopupLeft.png',
		'images/contactsPopupLeftToUp.png',
		'images/contactsPopup.png',
		'images/contactsPopupRight.png'
	]);

   };

/* for Mozilla */
if (document.addEventListener)
{
	document.addEventListener("DOMContentLoaded", init, false);
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
	document.write("<script defer src=js/ie_onload.js><"+"/script<");
/*@end @*/

/* for other browsers */
window.onload = init;

function preload(images) {
    if (typeof document.body == "undefined") return;
    try {

        var div = document.createElement("div");
        var s = div.style;
		    s.position = "absolute";
        s.top = s.left = 0;
        s.visibility = "hidden";
        document.body.appendChild(div);
		div.innerHTML = "<img src="" + images.join("" /><img src="") + "" />";
		var lastImg = div.lastChild;
		lastImg.onload = function() { document.body.removeChild(document.body.lastChild); };
	 }
	 catch(e) {
        // Error. Do nothing.
	}
}

-->

</script>

Демо пример. На баг в IE6 с шириной окна не обращай внимания — для демонстрации прелоада это не имеет значения.

Несколько упрощенный вариант, если используется jQuery
<script type="text/javascript">
<!--
jQuery(document).ready(function(){

	// preload images
    preload([
		'path-to/img1.png',
		'path-to/img2.png'
		]);

function preload(images) {
    if (typeof document.body == "undefined") return;
    try {

        var div = document.createElement("div");
        var s = div.style;
		    s.position = "absolute";
        s.top = s.left = 0;
        s.visibility = "hidden";
        document.body.appendChild(div);
		div.innerHTML = "<img src="" + images.join("" /><img src="") + "" />";
		var lastImg = div.lastChild;
		lastImg.onload = function() { document.body.removeChild(document.body.lastChild); };
	 }
	 catch(e) {
        // Error. Do nothing.
	}
}
});
-->
</script>

Проверено в: