[Хд] logo

Lazy loading ресурсов

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

Эффективная работа с ресурсами позволяет повысить скорость приложения. Lazy loading — простой подход оптимизации управления ресурсами.

Lazy loading

Работа с любым внешним ресурсом обычно состоит из двух этапов:

  • Подключение. Например, инициализация соединения с MySQL либо чтение файла настроек приложения.
  • Использование. Например, получение данных из MySQL либо использование подключенного компонента.

Lazy loading (ленивая загрузка) предусматривает откладывание первого этапа до наступления второго. Если второй этап никогда не наступит, то и первый выполнен не будет. Тогда, приложение получает экономию в случаях, когда ресурс не используется. Например, страницы приложения, которые не работают с базой данных или Javascript-библиотекой.

Ленивая инициализация соединений

Наиболее часто в приложения используется работа с базами данных (MySQL, Memcache, Redis и т.п.). Обычно подключение к базе инициализируется где-то на старте приложения:

mysql_connect('127.0.0.1', 'root', '123');
memcache_connect('127.0.0.1');

#...
mysql_query('SELECT * FROM users WHERE id = 7');

# Мы не использовали запросы к Memcache, хотя установили к нему соединение

Lazy loading в этом случае предусматривает установку соединения только в момент первого обращения к ресурсу. Например:

class mysql
{
	private static $connection;
	public static function query($sql)
	{
		if ( !self::$connection ) self::$connection = mysql_connect('127.0.0.1', 'root', '123');
		return mysql_query($sql);
	}
}

#...
mysql::query('SELECT * FROM users WHERE id = 7');

# Lazy loading для соединения с MySQL

Все обращения к MySQL должны будут проходить через класс mysql. Тогда установка соединения произойдет после первой попытки отправить запрос к базе. Такой же принцип будет работать для любых ресурсов, требующих установки соединений, например Memcache:

class memcache
{
	private static $connection;
	public static function get($key)
	{
		if ( !self::$connection ) self::$connection = memcache_connect('127.0.0.1');
		return memcache_get($key);
	}

	public static function set($key, $value)
	{
		if ( !self::$connection ) self::$connection = memcache_connect('127.0.0.1');
		return memcache_set($key, $value);
	}
}

#...
memcache::get('user7');

# Lazy loading для соединения с Memcache

Ленивое подключение компонент

Приложение обычно представляет из себя большую кучу файлов, в которых находятся различные компоненты (классы и функции). Подключение компоненты — это подключение файла. На каждой отдельной странице в лучшем случае используется 20...30% всех доступных компонент в приложении, а их подключение обычно выглядит так:

include 'platform.php';
include 'db.php';
include 'memcache.php';

#...
echo 'test page';

# Ни один из подключенных файлов не нужен на этой странице

Понято, что нет смысла подключать все файлы сразу. В PHP для этого предусмотрен механизм автозагрузки. Он позволяет выполнить определенную функцию при обращении к классу, который не существует.

Это позволит загружать файлы только тогда, когда они нужны. Для использования этого метода, необходимо организовывать все компоненты в классы. Часто удобно использовать просто статические классы:

spl_autoload_register(function($class) {
	include 'lib/' . $class . '.php';
});

mysql::query('SELECT * FROM users LIMIT 10');

# подключаем файл с классом mysql при первом обращении к нему

В реальных приложениях файлы классов часто находятся в разных папках. Для этого удобно использовать карту путей:

spl_autoload_register(function($class) {
	$map = [
		'mysql' => '/lib/db/mysql.php',
		'memcache' => '/lib/cache/memcache.php'
	];
	include $map[$class];
});

# Карту классов удобно собирать автоматически, например с помощью функции glob() и кэшировать в файл map.php

Конечно, на практике существуют обязательные для загрузки компоненты, которые нужно подключать всегда. В итоге, приложение будет структурировано так:

 # обязательные компоненты
include 'platform.php';


# карту классов берем из внешнего файла, который генерируем автоматически
$map = include 'map.php';

spl_autoload_register(function($class) {
	global $map;
	include $map[$class];
});


# ...

mysql::query('SELECT * FROM users WHERE id = 7');

Ленивое подключение файлов данных

Во многих приложениях существуют файлы, которые хранят данные для разных целей. Например:

  • Настройки приложения (параметры подключений к базам данных и т.п.)
  • Подготовленные переводы на разные языки
  • Карта классов для автозагрузки

Для таких случаев также удобно использовать ленивую загрузку. Например, пусть настройки приложения хранятся в файле вида:

return [
	'db' => [
		'host' => '127.0.0.1',
		'user' => 'root',
		'pwd' => '123'
	],
	# ...
];

# Пример файла настроек приложения

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

class config
{
	private static $data;

	public static function get($param)
	{
		if ( !self::$data ) self::$data = include 'config.php';
		return self::$data[$param];
	}
}

# ...
mysql_connect( config::get('db')['host'], config::get('db')['user'], config::get('db')['pwd'] );

# Загружаем настройки приложения только при первом обращении к ним

То же самое можно было бы сделать и для системы загрузки переводов:

class translate
{
	private static $data;

	public static function get($text, $language)
	{
		if ( !self::$data[$language] ) self::$data[$language] = include $language . '.php';
		return self::$data[$language][$text];
	}
}

# ...
translate::get('Привет', 'en');

Избирательное подключение Javascript

В современных приложениях размер Javascript обычно составляет несколько сотен килобайт. Клиентское кэширование позволяет значительно оптимизировать работу по загрузке Javascript'a. Однако, если человек впервые на Вашем сайте, его браузер будет загружать все ресурсы, т.к. в кэше пусто.

Современные приложения часто используют т.н. посадочные страницы (страницы, на которые попадают новые пользователи). Обычно они оптимизированы под рекламные кампании и содержат небольшое количество функционала (часто вообще статические).

Для таких страниц следует отключать загрузку Javascript (если он действительно не используется):

<html>
...
<? $disable_javascript = true; ?>
...
<? if ( !$disable_javascript ) { ?>
	<script src="/app.js"></script>
<? } ?>
</body>
</html>

Это может снизить объем необходимых для загрузки данных в несколько раз для нового посетителя.

Разгрузка фреймворка

Фреймворки добавляют довольно большой оверхед в работу любого приложения. Однако часто бывают ситуации, когда необходимо выполнить какое-то тривиальное действие. Например, увеличить счетчик в базе данных либо сбросить ключ в Memcache. В этом случае загрузка фреймворка может составлять 99% от всего времени, требуемого на выполнение этой операции. Если таких операций очень много (например, подсчет просмотра статьи), следует использовать разгрузку.

Разгрузка фреймворка — это специальная техника, с помощью которой можно выполнять простые операции без загрузки всей платформы. Представим, что подсчет просмотра для статей в блоге использует AJAX (следует делать именно так, если для показа самой статьи используется HTTP-кэш):

<html>
...
<script src="/track?id=7"></script>
...
</html>

Мы могли бы сделать отдельный файл track.php для подсчета просмотров вместо реализации на основе фреймворка:

mysql_connect('127.0.0.1', 'root', '123');
mysql_query('UPDATE posts SET views = views + 1 WHERE id = ' . intval($_GET['id']));

И изменить вызов в HTML:

...
<script src="/track.php?id=7"></script>
...

Однако, необходимость делать кучу отдельных файлов может создать проблемы. Например, при изменении параметров соединений, понадобится менять их в разных местах. Это неудобно.

Для удобства можно использовать кэш APC (вообще можно использовать любую внешнюю базу данных для этого). Тогда логика подсчета останется в самом фреймворке. Но мы дополним ее инструкцией разгрузки:

 # Инициализация и загрузка фреймворка

posts::track_pageview( $_GET['id'] );
apc_store('unload_track_' . $_GET['id'], [
	'connect' => config::get('mysql'),
	'query' => 'UPDATE posts SET views = views + 1 WHERE id = ' . intval($_GET['id'])
]);

# Считаем просмотр и сохраняем инструкцию в APC для будущего использования

Мы сохранили в APC параметры подключения к MySQL и запрос, который следует выполнять без загрузки фреймворка.

Теперь перед инициализацией фреймворка мы можем добавить обработку разгрузки:

# Считаем, что название страницы передается в параметре page
if ( $unload = apc_fetch('unload_' . $_GET['page'] . '_' . $_GET['id']) ) {
	mysql_connect($unload['connect']['host'], $unload['connect']['user'], $unload['connect']['pwd']);
	mysql_query($unload['query']);
	return;
}

# Инициализация и загрузка фреймворка

posts::track_pageview( $_GET['id'] );
apc_store('unload_track_' . $_GET['id'], [
	'connect' => config::get('mysql'),
	'query' => 'UPDATE posts SET views = views + 1 WHERE id = ' . intval($_GET['id'])
], 60*60);

# Если в ключе есть данные, мы выполним нужные действия без загрузки фреймворка

Использовать разгрузку фреймворка следует в тех случаях, когда имеет место большое количество повторных простых операций.

Самое важное

Ленивая загрузка ресурсов — обязательный атрибут производительных приложений.

  • Отложенная установка соединений и загрузка данных дадут 90% эффекта.
  • Избирательная загрузка Javascript может хорошо отразиться на вовлечении новых пользователей.
  • А разгрузка фреймворка позволит значительно снизить расход ресурсов железа на выполнение простых повторяющихся операций.
  read in english
[Хд]

Подписывайтесь на отборные материалы по продвинутой разработке

Google Email

Esc, чтобы подписаться позже