Несколько советов по оптимизации приложений на Angular JS
Что нам потребуется.
AngularJS
Версия < 1.3
В версии 1.3 появились существенные отличия. Включая однонаправленный биндинг - :: (два двоеточия), но она несовместима с bindonce.
Хотя по скорости ничем не уступает оптимизированному варианту. Если вы используете >= 1.3, то применяйте готовый однонаправленный биндинг вместо bo-*
Bindonce
url: https://github.com/Pasvaz/bindonce
Эта библиотека позволяет делать однонаправленный биндинг данных в шаблоны.
Если вам не требуется, чтобы какая-то переменная после своего отображения меняла значение (т.е. она не будет его менять в процессе работы), то это однозначно для вас.
Например у нас есть список книг, который приходит с сервера и обновляется целиком (опять же с сервера).
Что мы делаем в коде
<div ng-repeat="book in books"><span></span><div>
Имя книги не меняется и мы можем записать так
<div ng-repeat="book in books"><span bo-text="book.name"></span><div>
Так как при перезагрузке книг список books будет изменен полностью, то дерево будет перестроено заново и нам не нужно никак заботиться о том, что имя изменится.
Однако, если вы используете sly-repeat (ниже), то его нельзя сочетать с bindonce из-за агрессивного кеширования дерева дом этим самым слайрепитом.
Scalyr
url: https://github.com/scalyr/angular
post: http://blog.scalyr.com/2013/10/31/angularjs-1200ms-to-35ms/
Эта библиотека позволит вам минимизировать количество одновременно работающих вотчеров путем отключения ненужных. А так же реализовать умный ng-repeat с кешированием созданных dom-элементов (который, увы, не сильно дружит с bindonce. т.е. не дружит вовсе).
Оптимизации
Много скрытых элементов на странице
Это одна из ключевых оптимизаций для проекта. Так как нам часто приходится скрывать/показывать разные элементы.
Представим, что у нас есть какой-то код типа нижеследующего.
<div ng-show="condition"><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>
Если не предпринять дополнительных действий, то все то, что написано внутри скрытого дива будет выполняться (!), а это лишние такты процессора и нервные подергивания пользователя.
Что же мы можем сделать?
На помощь нам приходит sly-show+sly-prevent-evaluation-when-hidden. Эта убойная комбинация не будет запускать интерпретацию скрытых элементов.
<div sly-show="condition" sly-prevent-evaluation-when-hidden><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>
Переписав код таким образом мы получим тот самый профит: снижение нагрузки на клиент-сайд.
Но стоит заметить одну особенность директивы: она в своем решении интерпретировать или нет вложенный код опирается на наличие класса ng-hide. При инициализации приложения или перерисовке элементов этого класса нет. Поэтому мы можем увидеть что-то вроде этого:
Причина проста: у элементов при инициализации еще нет класса ng-hide. Он появится только после того, как отработают нужны контроллеры, а значит, что отрисовка элемента с нуля всегда будет отрисовывать и интерпретировать все скрываемые элементы (даже если они действительно скрыты).
Решение проблемы: принудительно добавить класс ng-hide всем элементам, которые находятся под связкой sly-show+sly-prevent-evaluation-when-hidden.
<div class="ng-hide"><!-- а тут много разных строчек с данными, которые выводятся на базе данных из ангуляра --></div>
На этом оптимизация неиспользуемого кода закончена.
Оптимизация биндингов
Под этим подразумевается однократное добавление в шаблон переменной.
Как в примере bindonce с книгами (выше) нам может не требоваться функционал двунаправленного отображения.
Поэтому все подобные участки мы заменяем однонаправленным биндингом.
-> <div bo-text=”var”></div>
Путь оптимизации: вместо элемента создается контейнер, на который выполняется отображение текста элемента. Так как в биндванс не существует шаблонной конструкции для биндинга. Естественно, что там может быть любой тег и не обязательно div. Самое главное - наличие контейнера.
ng-href="text" -> bo-href="’text’ + var"
А так мы оптимизировали ссылки.
Важно заметить, что ng-* и bo-* работают с биндингами различным образом: если первый работает по внутриангуляровским правилам интепретации, то второй просто выполняет .eval() на строку. Поэтому в bo-* мы работаем с обычными строками и переменными.
Аналогичным образом работаем и с другими атрибутами. Если есть аналог биндинга.
Если аналога нет, то используем конструкцию bo-attr-<имя кастомного="" атрибута="">имя>
<div bo-attr bo-attr-isbn="’’"></div>
Здесь кроется еще одна важная особенность обработки атрибутов библиотекой: они рассматриваются как строки. И если в атрибуте текст, что его надо представить в виде текста (то же характерно для любого юиндига bo-*). Мы привыкли чаще всего работать с числами, но не стоит забывать, что есть еще буквы.
Поэтому если не уверены в содержимом переменной - пишите строковое предствление: ‘’, а не var.
Вложенность элементов
На картинке профайлера (выше) вы можете увидеть гребенку из кучи вложенных вызовов функций. Если мы посмотрим на эти вывовы под микроскопом отладчиком, то увидим, что это рекурсивный обход дерева dom.
Чем меньше у вас уровней вложенности, тем лучше. Особенно это касается кода, который расположен в циклах.
Так что старайтесь сократить вложенности.
Дополнительная оптимизация при обработке скрытых элементов
У sly-prevent-evaluation-when-hidden есть одна большая проблема: она опирается только на наличие класса ng-hide, что не совсем уместно если видимость элемента контролируется каким-то другим классом.
В таком случае можно (вообще нужно всегда) использовать следующее расширение директивы (подключаем после подключения scalyr).
/**
* Extended version of slyPreventEvaluationWhenHidden from scalyr.
* This version prevent evaluation not only when element has class ng-hide.
* If element is hidden with "display: none" evaluation is prevented.
*/
angular.module('sly')
.directive('slyPreventEvaluationWhenHidden', function () {
return {
restrict: 'A',
// We create a new scope just because it helps segment the gated watchers
// from the parent scope. Unclear if this is that important for perf.
scope: true,
compile: function compile(tElement, tAttrs) {
return {
// We need a separate pre-link function because we want to modify the scope before any of the
// children are passed it.
pre: function preLink(scope, element, attrs) {
scope.$addWatcherGate(function hiddenChecker() {
// Should only return true if the element is not hidden.
return !element.hasClass('ng-hide') && element.is(":visible") ;
}, function hiddenDecider(watchExpression, listener, equality, directiveName) {
// Make an exception for slyShow.. do not gate its watcher.
if (isDefined(directiveName) && (directiveName == 'slyShow'))
return false;
return true;
});
},
};
},
};
});
PHP: Cannot determine default value for internal functions
Зашла речь про небезызвестный htmlspecialchars, но не просто у нем, а о нем и о работе с htmlentities. Если передать в этот метод строку с этими энтитями, то получим двойное перекодирование.
echo htmlspecialchars('"текст"');
&quot;текст&quot;
Именно для того, чтобы избежать подобного был добавлен параметр double_encode, который по дефолту всегда true.
Но речь не об этом.
Речь о том, что до этого параметра в спецификации стоит еще два других с дефолтными значениями.
На ум приходит использовать рефлексию для получения дефолтных значений и передачи их в функцию. Но не все так просто.
Сначала напишем код, который будет вызывать функции принимая в качестве параметра ассоциативный массив с онными.
function call_user_func_params(callable $func, array $params = array()) {
if (!is_callable($func)) {
throw "func is not callable";
}
$reflection = new ReflectionFunction($func);
$funcParams = array();
foreach ($reflection->getParameters() as $parameter) {
$name = $parameter->getName();
if (__DEBUG__) {
var_dump($parameter->getName());
var_dump($parameter->isOptional());
}
if (array_key_exists($name, $params)) {
array_push($funcParams, $params[$name]);
} elseif ($parameter->isOptional()) {
array_push($funcParams, $parameter->getDefaultValue());
} else {
throw new Exception("Value for parameter {$name} not found");
}
}
return call_user_func_array($func, $funcParams);
}
А теперь воспользуемся ей.
function foo($bar = 'bar', $baz = 'baz') {
echo "{$bar}, {$baz}\n";
}
call_user_func_params('foo', array('baz' => 'xyz'));
Выводит то, что и ожидалось.
bar, xyz
А теперь так.
call_user_func_params('htmlspecialchars', array('string' => ""текст"", 'double_encode' => false));
Облом-с.
PHP Fatal error: Uncaught exception 'ReflectionException' with message 'Cannot determine default value for internal functions' in file.php
А все потому, что пхп обрабатывает встроенные функции иначе, нежели написанные кривыми руками программиста. :)
А еще документация нас открыто уведомляет о том, что именя параметров реальные (я про встроенные функции) могут отличаться от того, что написано в спецификации.
Openkinect+linux: поддержка звука
Есть такая демка во openkinect - micview. Для её использования нужна прошивка из состава kinect sdk или та, которая идет с Xbox360.
Подробнее можно глянуть официальный мануал.
А тут немного о том, где же эту прошивку взять (вики шлет в разные списки рассылки или гугл).
Если мы в linux, то нам нужен audios.bin из состава обновления xbox360. В windows нам нужен тот, который идет в составе kinect sdk (из-за ограничения стека usb в винде прошивка от xbox360 нормально работает только в linux).
- скачиваем Обновление для xbox360 с audios.bin
- скачиваем скрипт extract360.py для распаковки ресурсов xbox
- вытаскиваем из скачанного архива файл FFFE07DF00000001
- натравливаем
$ extract360.py FFFE07DF00000001
- кладем audios.bin в ~/.libfreenect
Fedora+Nvidia=CUDA
Итак. Мы хотим получить рабочую инсталляцию на Fedora (в моем случае - RFRemix и с этим связано несколько багов).
Для этого нам надо:
- видеокарту nvidia :)
- установленный репозитарий cuda (отсюда)
- заметку о том, как восстановить plymouth после установки драйвером nvidia
1 - Ставим
# yum install cuda
Этот метапакет поставит все, что должно идти в комплекте (включая свежие дрова)
2 - Удаляем kmod-nvidia
Если у вас уже были проинсталены дрова невидии, то нужно удалить имеющийся kmod. Так как он может содержать несовместимую с текущими библиотеками версию модуля
# yum remove kmod-nvidia
3 - Пересобираем akmod
# akmod -kernel $(uname -r)
Тут может потребоваться доставить kernel-headers
4 - Пересобираем initrd
Для этого воспользуемся dracut
# dracut --force
5 - Ребут :)
6 - Можно собрать семплы
Замечу, что делать это надо под рутом или проставив соотвествующие права на папки. Так как мейкфайлы сильно завязаны на относительные пути.
# cd /usr/local/cuda-6.5/samples/5_Simulations/fluidsGL
# make
7 - А вот тут нас поджидает облом (RFRemix)
>>> WARNING - libGL.so not found, refer to CUDA Samples release notes for how to find and install them. <<<
>>> WARNING - libGLU.so not found, refer to CUDA Samples release notes for how to find and install them. <<<
>>> WARNING - libX11.so not found, refer to CUDA Samples release notes for how to find and install them. <<<
>>> WARNING - libXi.so not found, refer to CUDA Samples release notes for how to find and install them. <<<
>>> WARNING - libXmu.so not found, refer to CUDA Samples release notes for how to find and install them. <<<
Ага!
Смотрим в findgllib.mk
ifeq ("$(OSLOWER)","linux")
# first search lsb_release
DISTRO = $(shell lsb_release -i -s 2>/dev/null | tr "[:upper:]" "[:lower:]")
DISTVER = $(shell lsb_release -r -s 2>/dev/null)
ifeq ("$(DISTRO)","")
# second search and parse /etc/issue
DISTRO = $(shell more /etc/issue | awk '{print $$1}' | sed '1!d' | sed -e "/^$$/d" 2>/dev/null | tr "[:upper:]" "[:lower:]")
DISTVER= $(shell more /etc/issue | awk '{print $$2}' | sed '1!d' 2>/dev/null
endif
ifeq ("$(DISTRO)","")
# third, we can search in /etc/os-release or /etc/{distro}-release
DISTRO = $(shell awk '/ID/' /etc/*-release | sed 's/ID=//' | grep -v "VERSION" | grep -v "ID" | grep -v "DISTRIB")
DISTVER= $(shell awk '/DISTRIB_RELEASE/' /etc/*-release | sed 's/DISTRIB_RELEASE=//' | grep -v "DISTRIB_RELEASE")
endif
endif
ifeq ("$(OSUPPER)","LINUX")
# $(info) >> findgllib.mk -> LINUX path <<<)
# Each set of Linux Distros have different paths for where to find their OpenGL libraries reside
UBUNTU_PKG_NAME = "nvidia-340"
UBUNTU = $(shell echo $(DISTRO) | grep -i ubuntu >/dev/null 2>&1; echo $$?)
FEDORA = $(shell echo $(DISTRO) | grep -i rfremix >/dev/null 2>&1; echo $$?)
RHEL = $(shell echo $(DISTRO) | grep -i red >/dev/null 2>&1; echo $$?)
CENTOS = $(shell echo $(DISTRO) | grep -i centos >/dev/null 2>&1; echo $$?)
SUSE = $(shell echo $(DISTRO) | grep -i suse >/dev/null 2>&1; echo $$?)
Еще раз ага!
Смотрим как определяется тип дистрибутива.
Это команда
awk '/ID/' /etc/*-release | sed 's/ID=//' | grep -v "VERSION" | grep -v "ID" | grep -v "DISTRIB"
А у меня она выдает rfremix. А значит такого таргета ни разу нет в списке. :)
Ну что делать-то? меняем fedora на rfremix в findgllib.mk и радуемся.
Собралось! Но…
8 - А что у нас с библиотеками?
$ ./fluidsGL
./fluidsGL: error while loading shared libraries: libcufft.so.6.5: cannot open shared object file: No such file or directory
Еще раз. У нас для ldconfig не заданы пути, где лежит libcufft.so.6.5
$ find /usr/ -name libcufft.so.6.5
/usr/local/cuda-6.5/targets/x86_64-linux/lib/libcufft.so.6.5
$ grep -R /usr/local/cuda-6.5/targets/x86_64-linux/lib /etc/ld.so.conf.d/
Пусто.
# echo "/usr/local/cuda-6.5/targets/x86_64-linux/lib" > /etc/ld.so.conf.d/cuda-lib64.conf
# ldconfig
9 - Наслаждаемся
Firefox+Linux+MiddleButton
Есть такая особенность у сборки Firefox в Linux (как под виндой - не знаю) - если нажать среднюю кнопку (не на ссылке, а просто в пределах страницы), то в адресную строку будет вставлено содержимое буфера обмена.
Очень и очень раздражающая фича.
Но отключается она очень просто.
- Идем в about:config.
- Находим настройку middlemouse.contentLoadURL и выставляем ее в false (или создаем - это параметр “логическое”)
Вопреки советам в сети за подобное поведение параметр middlemouse.paste не отвечает.