Всем доброго времени суток. Релиз Angular.js 2.0 приближается, а проблемы с производительностью первой версии все еще остаются. Эта статья посвящена оптимизации Angular.js приложений и будет полезна как начинающим, так и тем, кто уже использует этот фреймворк, но еще не сталкивался с проблемами его производительности.

Немного простой теории


Как известно, Angular.js это декларативный фронт-енд фреймворк, предоставляющий удобный байндинг данных. Конечно, не будем забывать так же о тестируемости, поддерживаемости и условной читабельности Angular.js приложений, но, в контексте этой статьи, это не важно.

Итак, одна из особенностей этого фреймворка — удобный байндинг данных «прямо из коробки». Однако за счет чего он работает? Если упрощенно, то связывание данных в Angular.js держится на scope, digest, и watcher.

Scope (или $scope) это объект, содержащий данные и/или методы, которые необходимо будет отобразить или использовать на странице, а так же ряд технических свойств, таких как его идентификатор, ссылка на родительский scope и так далее.

Watcher это объект, хранящий в себе значение заданного нами выражения и колбек функцию, которую нужно вызвать если это выражение изменится. Массив watcher-ов находится в $scope.$$watchers.

Digest — поочередный обход всех watcher-ов и вызов колбек функций тех, значение которых изменилось. Если в результате дайджеста, хотя бы одно значение было изменено, дайджест будет запущен еще раз. Поэтому часто дайджест запускается два и больше раз. Если дайджест будет запущен более 10 раз — Angular выбросит исключение.

Watcher-ы хранятся в scope и посмотреть их можно перебрав $scope.$$watchers. В основном они создаются автоматически, однако их можно создать и вручную. Директивы используют либо scope контроллера, либо создают свой. Соответственно watcher-ы директив стоит искать в их scope.

Очевидно, чем больше watcher-ов, тем дольше длится цикл дайджеста. А, поскольку, javascript язык однопоточный, то при значительной продолжительности дайджеста, приложение начнет «тормозить». Тем более что дайджест это не просто обход массива, но и вызов колбеков для тех выражений, значение которых поменялось. Считается, что angular гарантирует беспроблемную работу до тех пор, пока страница содержит до, примерно, двух тысяч watcher-ов. И, хотя эта цифра звучит достаточно внушительно, достигнуть ее можно достаточно быстро.

забавные цифры
Вчера был поставлен мой новый рекорд — я увидел 65000 вотчеров на одной странице. И, как мне кажется, это был не предел.

Например вот этот маленький кусочек разметки на десять строк создаст восемьдесят watcher-ов плюс десять отдельных scope

<table>
    <tbody>
    <tr data-ng-repeat="cartoon in model.cartoons" data-ng-class="{even: $even, dd:$odd}" 
         data-ng-hide="cartoon.isDeleted">
        <td>{{cartoon.no}}</td>
        <td>{{cartoon.name}}</td>
        <td>{{cartoon.description}}</td>
        <td>{{cartoon.releaseDate}}</td>
        <td>{{cartoon.mark}}</td>
        </tr>
    </tbody>
</table>

А теперь к практике


Первая проблема с производительностью лежит в плоскости количества watcher-ов. И чтобы ее решить, мы должны четко понимать, что создавая любое выражение привязки, мы создаем watcher. Ng-bind, ng-model, nd-class, ng-if, ng-hide и так далее — все они создают объект-наблюдатель. И, если, в одиночку они не представляют угрозы, то их использование вместе с ng-repeat, как видно в примере выше, способно очень быстро собрать целую армию маленьких убийц нашего приложения. А самую большую опасность представляет полностью динамические таблицы. Именно они способны плодить watcher-ов в масштабах, достойных г-на Исиро Хонды.

Поэтому, первый (а иногда и последний) шаг в оптимизации количества watcher-ов лежит в анализе изменяемости данных, которые отображаются на странице. Другими словами, стоит следить только за теми данными, которые должны измениться. Очень часто возникает ситуация, когда данные нужно просто вывести пользователю. Например, нужно отобразить список всех возможных покупок, или вывести статичный текст, на который ни пользователь, ни сервер влиять не будут. Поэтому, в angular 1.3 появился специальный синтаксис для одноразовой привязки данных, который выглядит так:

data-ng-bind="::model.name"
или так
data-ng-repeat=«model in ::models»

Это выражение означает, что, как только данные будут посчитаны и выведены на страницу, watcher отвечающий за это выражение будет удален. Комбинируя одноразовую привязку с ng-repeat можно получить существенную экономию watcher-ов в нашем приложении. Правда здесь есть один нюанс. Если данных, участвующих в выражении привязки нет (например сервер прислал null вместо названия товара), то watcher удален не будет. Он будет «ждать» данные, и только потом удален.

Второй шаг заключается в разделении ответственности. Другими словами — далеко не все должно быть Angular. В примере выше была использована директива ng-class для установки CSS классов четным и не четным строкам. Заменив ее на CSS правило tr:nth-child(even), мы избавимся от лишних watcher-ов, к тому же получим выигрыш (крайне малый) в быстродействии. Аналогичная ситуация и с событиями, такими как ng-mouseover и ng-mouseleave (их использование вызывает и другие проблемы с производительностью — о чем ниже). Зачастую их обработку можно возложить на свою директиву плюс jquery. Кстати о jQuery и директивах. Иногда, таблицу или список следует перерисовать только в одном или двух случаях. В таком случае намного эффективнее будет использовать свою директиву вместе с одним или двумя вручную созданными watcher-ами. Если какая-либо функциональность не вызывает перерисовку данных модели — это первый признак того, что ее можно сделать не в Angular style. Это не всегда нужно, но решение должно приниматься осознано.

Приведу упрощенный пример. Пускай у нас есть два списка товаров — доступные, и те, которые выбраны пользователем. Очевидно, что первый список у нас будет, во-первых, большим, а, во-вторых, статичным, так как после его загрузки, ни пользователь, ни сервер его менять не будут. Значит, здесь мы можем использовать «одноразовый» ng-repeat. А вот второй список динамичен и постоянно изменяется пользователем. Поэтому здесь нам использовать одноразовую привязку данных не стоит. Хотя, если актуальные данные нам нужны не каждую секунду, а только в момент нажатия на кнопку «купить», то здесь тоже можно сделать статику, возложив ответственность за сбор финальных данных на директиву. Нужно ли тратить ресурсы на такую оптимизацию — смотрите по текущей ситуации и размерам списков.

И, наконец, третий шаг заключается в правильном сокрытии неиспользуемой разметки. Из коробки, Angular.js предоставляет ng-show/ng-hide которые прячут или показывают нужные нам части страницы. Однако, связанные watcher-ы никуда не исчезают и участвуют в дайджесте, как и прежде. А вот использование ng-if полностью вырезает элементы из Dom-дерева вместе с соответствующими watcher-ами. Правда удаление элементов из Dom тоже не самая быстрая процедура, так что использовать ng-if стоит к тем частям разметки, которые будут скрываться/показываться не слишком часто, где «слишком» — зависит от конкретного приложения.

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

Вторая, хотя и меньшая проблема с производительностью заключается в слишком частом вызове дайджеста.

Обычно, проблемы с дайджестом возникают, когда количество watcher-ов приближается к критическому, но слишком частый запуск дайджеста может так же создать проблемы. Как известно, дайджест, это не только обход массива watcher-ов, но и выполнение колбеков для изменившихся выражений. К тому же, часто дайджест запускается несколько раз подряд, еще более замедляя производительность. Ng-model будет запускать дайджест после каждой введенной буквы. Например ввод этого слова из пятидесяти пяти букв Тетрагидропиранилциклопентилтетрагидропиридопиридиновые запустит дайджест минимум сто десять раз. Как только пользователь введет первую букву будет запущен дайджест. Поскольку в процессе его исполнения будет обнаружено, что данные модели изменились, дайджест будет выполнен повторно. Кстати, дайджест будет вызван не только на scope контроллера, а и на других scope страницы. Поэтому ng-model может стать довольно серьезной проблемой.

Простым решением, будет добавить debounce-параметр который отложит вызов дайджеста на указанное время. Аналогичная ситуация с использованием ng-mouseenter, ng-mouseover и так далее. Они могут запускать дайджест слишком часто, что приведет к падению производительности приложения.

Поэтому нужно уделять особое внимание участкам, с помощью которых пользователь может (пусть и не явно) вызывать дайджест в приложении, таких как input, области наведения и так далее. А если вам нужно вызвать дайджест вручную, старайтесь это делать тогда, когда внесено максимальное количество изменений, что бы дайджест подхватил их все за один проход.

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

  • По возможности используйте ng-bind вместо {{}}. Строковое выражение привязки обрабатывается примерно в два раза медленнее по сравнение с ng-bind. К тому же использование ng-bind избавляет от необходимости использования ng-cloak.
  • Избегайте использования сложных функций в выражениях привязки. Функции, указанные в выражении привязки запускаются каждый раз при запуске дайджеста. А, поскольку, дайджест часто запускается неоднократно, выполнение этих функций может существенно замедлить рендер страницы.
  • Используйте фильтры только в том случае, если обойтись без них нельзя. Если функции, указанные в выражении привязки выполняются один раз за дайджест, то функция фильтра выполняется два раза за дайджест, для каждого выражения. Лучше всего фильтровать данные в контроллере или сервисе.
  • По возможности, используйте $scope.$digest() вместо $scope.$apply(). Дело в том, что первая функция запустит дайджест только в пределах скоупа, на котором она была вызвана, а вторая — на всех scope, начиная с rootScope. Очевидно, что первый дайджест пройдет быстрее. Кстати $timeout в конце вызовет именно $rootScope.$apply().
  • Помните о возможности отложенного вызова дайджеста на вводе пользователя, задавая параметр debounce: data-ng-model-options="{debounce: 150}"
  • Старайтесь избегать использования ng-mouse-over и подобных директив. Вызов этого события запустит дайджест, а природа таких событий такова, что они могут быть вызваны многократно за короткий промежуток времени.
  • Создавая свои watcher-ы, не забывайте сохранять функцию их удаления и вызывать ее сразу, как только watcher-ы перестанут быть нужны. Кроме того, избегайте установки флага objectEquality в true. Это вызывает глубокое копирование и сравнение нового и старого значений для определения необходимости вызова колбек функции.
  • Не стоит хранить ссылки на Dom элементы в scope. Он содержат ссылки на родительский и дочерние элементы, т.е. по сути, на весь дом элемент. А, значит, дайджест будет пробегать по всему Dom дереву проверяя какой из объектов поменялся. Не стоит говорить, насколько это затратно.
  • Пользуйтесь параметром track by в директиве ng-repeat. Во первых это быстрее, а во вторых убережет от ошибки duplicates in a repeater are not allowed , которая возникает, когда мы пытаемся вывести одинаковые объекты в списке.

На этом статья закончена. Более подробно можно почитать по ссылкам ниже:


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

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


  1. corvette
    22.07.2015 10:38
    +6

    Я поражаюсь скорости работы современных браузеров. Они уже справляются с подобным неэффективным расходованием ресурсов.


    1. Drag13 Автор
      22.07.2015 10:59

      Не думаю, что это заслуга только браузера. Железо довольно мощное для таких относительно простых операций, как переборы массива и вызов колбеков. А вот оптимизация вставки в DOM это уже действительно заслуга браузера.


  1. lolmaus
    22.07.2015 10:56
    +3

    Если в результате дайджеста, хотя бы одно значение было изменено, дайджест будет запущен еще раз.

    У меня было getter-свойство, которое возвращало строку. Я столкнулся с известной проблемой, что примитивы по scope'ам не передаются, а копируются.

    Поэтому я изменил getter-свойство так, чтобы оно возвращало объект со строкой в единственном свойстве… Angular зациклился и упал.

    Видать, тот факт, что при каждом считывании свойства возвращался новый инстанс объекта, Angular не пережил.

    И это только один из множества примеров из удивительной жизни Angular, которые делают мою жизнь как разработчика несчастной. Но мне недолго осталось мучиться: перехожу работать в бостонскую команду, использующую Ember.

    Мое вольное сравнение Angular и Ember читайте тут: habrahabr.ru/post/255769/#comment_8376441 Стоит ли оформить это в статью?


    1. Drag13 Автор
      22.07.2015 11:06
      +1

      Да, Angular весьма своебразен в некоторых моментах. Но мне в общем то нравится, хотя и не везде.
      А насчет статьи — конечно стоит.


    1. corvette
      22.07.2015 11:17
      +1

      Однозначно стоит, это лучшее описание Angular, которое я видел. Надеюсь и про Ember все верно, у меня нет опыта работы с ним.


      1. DigitalSmile
        22.07.2015 12:11

        del


    1. DigitalSmile
      22.07.2015 12:12

      Да, конечно, было бы интересно почитать развернутую версию.


    1. elepner
      22.07.2015 12:44
      +1

      Что же вы так хейтите бедный angular, как будто он вас обидел чем-то.

      перехожу работать в бостонскую команду, использующую Ember
      — Бе бе бе, уйду от тебя к другому.

      А если серьезно, то опыта работы с Ember у меня нет, поэтому сравнивать их с Angular не могу, но как человеку, пришедшему из мира WPF data-binding для меня является самой естественной сущностью и по одному взгляду на декларативную разметку сразу понятно, как изменится состояние приложения, если я кликну на эту кнопку.

      На мой взгляд, кодинг с постоянной мыслью «за магию всегда надо платить» поможет избежать всех этих ошибок. Для меня эта статья оказалась подтверждением, что я делал все правильно, хотя нигде явно про это не читал. Правда, некоторые моменты для меня оказались новыми, за что автору спасибо. Как говорится, век живи — век учись.


      1. lolmaus
        22.07.2015 13:57
        +7

        data-binding для меня является самой естественной сущностью

        Представьте себе, в Ember тоже есть data-binding, и реализован он в тысячу раз лучше, чем в Angular, как с точки зрения производительности, так и удобства.

        А что касается декларативности, то JS-код в Ember пишется декларативно благодаря в ленивым, кэшируемым вычисляемым свойствам (computed properties). В Angular сплошная императивщина, на которую после Ember смотреть противно.

        по одному взгляду на декларативную разметку сразу понятно, как изменится состояние приложения, если я кликну на эту кнопку.

        Это самый большой п#@дёж, который можно услышать в пользу Angular.

        В реальных проектах вы сталкиваетесь с HTML-элементами, имеющих десятки атрибутов. И глядя на разметку, совершенно непонятно, какие из них являются кастомными директивами (а также что они делают и в каком порядке выполняются), какие несут значения в виде строк, какие eval'ятся (eval'ятся, Карл!).

        DOM становится похож на новогоднюю елку. У меня уходит по 15-30 секунд только на то, чтобы в инспекторе браузера найти глазами имя класса данного элемента.

        кодинг с постоянной мыслью «за магию всегда надо платить»

        Вот за что приходится платить, так это за Angular'овский dirty-checking. Сделать на Angular тяжелый, сложный вэб-интерфейс просто невозможно. Чтобы справиться с тормозами, приходится отказываться от data-binding'а и обновлять DOM по-старинке.


        1. Drag13 Автор
          22.07.2015 14:04
          +2

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


        1. elepner
          22.07.2015 14:25

          Вот что я могу ответить со своей колокольни:

          Представьте себе, в Ember тоже есть data-binding, и реализован он в тысячу раз лучше, чем в Angular, как с точки зрения производительности, так и удобства.


          Мой друг использует Ember в своём проекте. Каждый день слышу как он тормрозит, как отожрал кучу памяти, как подкачал лишние данные. В нашем же проекте (привет Вилларибо и Виллабаджо) (инструмент просмотра и планирования электрических сетей) я прошел курс молодого бойца по JS после десктопа и вперед, колбасить код, я не думал об оптимизации от слова совсем, за исключением здравого смысла, и все прекрасно работает. Более того у нас главное требование — работа под мобильными устройствами с адаптивным дизайном. И оно работает на iPhone 4S без единого нарекания.
          В реальных проектах вы сталкиваетесь с HTML-элементами, имеющих десятки атрибутов. И глядя на разметку, совершенно непонятно, какие из них являются кастомными директивами (eval'ятся, Карл!)


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

          какие несут значения в виде строк, какие eval'ятся (eval'ятся, Карл!).

          Оператор :: убирает данную проблему.

          Сделать на Angular тяжелый, сложный вэб-интерфейс просто невозможно.

          На мой взгляд функционал нашего приложения будет даже больше, чем у, например, 2gis.ru. Не знаю, считаете ли вы его достаточно сложным, но у нас все работает хорошо, а в стеке продуктов компании есть еще множество решений на Angular и оно работает тоже отлично.


          1. lolmaus
            22.07.2015 14:45

            Традиционно считается, что потолком для Angular являются 2000 watcher'ов.

            Если их больше — вкладка начинает тормозить. Причем тормозит она непрерывно, а не только когда пользователь совершает действие. В особо запущенных случаях вместе со вкладкой тормозит вся ОС — процессор всё время занят.

            Когда мы заменили традиционную пагинацию наивной реализацией бесконечной прокрутки, то стало возможно уронить браузер, просто зажав клавишу пробел. Удаление контента с противоположного конца вьюхи почему-то не решило проблему, пришлось переписать всю вьюху на старом добром jQuery.


            1. Drag13 Автор
              22.07.2015 14:50

              Двух тычяч вотчеров при грамотном подходе хватает. У нас сейчас есть страница на 4 000 (ждет рефакторинга) и ничего, все работает.


            1. DigitalSmile
              22.07.2015 14:57
              +1

              Традиционно считается, что потолком для Angular являются 2000 watcher'ов.

              Есть очень хороший и дельный ответ самих разработчиков на эту тему. Цитата:

              What about performance?

              So it may seem that we are slow, since dirty-checking is inefficient. This is where we need to look at real numbers rather than just have theoretical arguments, but first let's define some constraints.

              Humans are:

              Slow — Anything faster than 50 ms is imperceptible to humans and thus can be considered as «instant».

              Limited — You can't really show more than about 2000 pieces of information to a human on a single page. Anything more than that is really bad UI, and humans can't process this anyway.

              So the real question is this: How many comparisons can you do on a browser in 50 ms? This is a hard question to answer as many factors come into play, but here is a test case: jsperf.com/angularjs-digest/6 which creates 10,000 watchers. On a modern browser this takes just under 6 ms. On Internet Explorer 8 it takes about 40 ms. As you can see, this is not an issue even on slow browsers these days. There is a caveat: the comparisons need to be simple to fit into the time limit… Unfortunately it is way too easy to add a slow comparison into AngularJS, so it is easy to build slow applications when you don't know what you are doing. But we hope to have an answer by providing an instrumentation module, which would show you which are the slow comparisons.

              Мне кажется, это отражает реальную суть вещей — если у Вас такое количество информации на странице и при этом все _постоянно_ меняется, то может стоит задуматься над изменением архитектуры фронтенда?


              1. lolmaus
                22.07.2015 15:04
                -1

                You can't really show more than about 2000 pieces of information to a human on a single page.

                Ага, а 640 килобайт оперативки достаточно любому пользователю.

                Мне кажется, это отражает реальную суть вещей — если у Вас такое количество информации на странице и при этом все _постоянно_ меняется

                В том-то и дело, что оно не меняется постоянно. Но Angular все равно всё пересчитывает n раз в секунду.

                стоит задуматься над изменением архитектуры фронтенда?

                Ага. Перешел на Ember (React, Meteor, Derby, Knockout… подставьте любой), проблема исчезла.


                1. Drag13 Автор
                  22.07.2015 15:11

                  Если код не меняется, или меняется не часто можно использовать одноразовую привязку и перерисовку on-demand через директиву.


                1. DigitalSmile
                  22.07.2015 15:16
                  +1

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

                  Честно говоря Ваши аргументы не убедительны для меня пока.


                  1. lolmaus
                    22.07.2015 15:19
                    -1

                    Всё, что вы написали, в проекте присутствует. Вместо того, чтобы нанять штатного ангулярщика, руководство решило отдать разработку на аутсорс. А аутсорсерам на качество кодовой базы традиционно насрать.

                    Но это не отменяет проблемности самого Angular. Когда на одном фрэймворке трудно сделать хорошо, а на другом трудно сделать плохо, это какбе намекает.


                    1. DigitalSmile
                      22.07.2015 15:45
                      +1

                      Спасибо за ссылку. Код минифицирован конечно, но выскажу предположение, что у Вас обилие биндинга, связанного с анимационными объектами (которые кстати в base64 лежат в скриптах).

                      Но это не отменяет проблемности самого Angular. Когда на одном фрэймворке трудно сделать хорошо, а на другом трудно сделать плохо, это какбе намекает.

                      Не отменяет, проблемы есть везде и разработчики или дают аргументированное объяснение почему было сделано так, а не иначе, или исправляют/планируют к исправлению проблему. Это, на мой взгляд, и отличает «хороший» инструмент от «плохого». Из-за таких вот проблем появилось одностороннее связывание и начали делать Angular 2.
                      Не соглашусь с фразами «трудно сделать хорошо» и «трудно сделать плохо», все зависит от задачи, исполнителя, бюджета и множества других факторов. Так можно сказать о любом инструменте.


              1. vintage
                23.07.2015 14:32
                -2

                Во время скроллинга 50ms — это слишком много. У вас есть лишь 16ms за которые должен отработать полный цикл (обновление данных, обновление дом, рендеринг изменившихся участков). Причём не на быстрой машинке разработчика, а даже на относительно медленной машинке пользователя. Это я вам говорю, как человек, который реализовывал динамические списки на десятки тысяч элементов с ленивой подгрузкой данных, рендерингом только видимого и тд.

                2000 элементов вам запросто может сделать на странице обычный табличный процессор. Плохо, когда фреймворк решает что такое хороший ui, а что такое плохой :-) Тем более что для «показа только видимого» нужно значительно усложнять логику работы приложения, так что не стоит этого делать, если можно этого не делать. Порог этой необходимости в angular — ниже.

                Одноразовые биндинги — это вообще жёсткие костыли.


                1. Drag13 Автор
                  23.07.2015 15:16
                  +1

                  Одноразовые биндинги — это вообще жёсткие костыли.

                  Обоснуйте пожалуйста это утверждение.


                  1. vintage
                    23.07.2015 15:31
                    -1

                    Вы в шаблоне решаете будут ли вообще обновляться определённые данные, а не в том месте, где они собственно устанавливаются или меняются. Нарушается принцип единственно ответственности. Повышается хрупкость.


                    1. Drag13 Автор
                      23.07.2015 15:36
                      +1

                      Когда Вы пишите

                      <span> Hello, User! </span>

                      Вы точно так же, в шаблоне, решаете, что эти данные не будут меняться.
                      В чем разница?


                      1. vintage
                        23.07.2015 18:36
                        -1

                        В данном случае «данные» находятся в самом шаблоне. А при одноразовом биндинге данные берутся извне. Ломать реактивность ради производительности только потому, что реактивность реализована криво — это и есть костыли. Нормальная реактивность таких костылей не требует.


                        1. Drag13 Автор
                          23.07.2015 18:39
                          +1

                          Просто отделите «реактивность» от «вывода статического текста» и все будет ок. У Ангуляра есть свои костыли (куда же без них) но одноразовый байндинг это прекрасная вещь.


                          1. vintage
                            23.07.2015 18:46

                            На прошлом проекте у нас чтобы сменить язык интерфейса приходилось перезагружать всю страницу. Вот вам и «статический текст, который никогда не меняется». Аналогичная фигня была с флагами включащими «бета-фичи».


                            1. Drag13 Автор
                              23.07.2015 19:03
                              +1

                              Ок, если Вам нужно менять язык интерфейса значит Вам нужен не статичный текст. Тут либо то, либо то. Нельзя быть «наполовину беременной».


                              1. vintage
                                23.07.2015 20:39
                                -2

                                Я бы предпочёл не думать о том, потребуется мне менять язык интерфейса или нет, когда я пишу маленькую реиспользуемую компоненту :-)


                            1. Fesor
                              23.07.2015 21:07

                              можно юзать кастомные bind-once и будет счастье. Пользуюсь как раз таки для i18n. Создает один ватчер за локалью на все лэйблы.


                              1. vintage
                                24.07.2015 14:25
                                -1

                                Нужно больше костылей :-)


                                1. Fesor
                                  24.07.2015 14:35

                                  Я не сказал бы что это кастыль. В core репозитории уже давно ведется разговор о лэйблед ватчерах, это тип пруф оф концеп. Штука довольно полезная. Еще обсуждался вариант дать возможность реинициировать bindOnce когда душе угодно и отдать управление на откуп разработчика. Это бы позволило без изменений в коде проделать такую операцию.

                                  С другой стороны, это не серебрянная пуля, и есть куча других моментов с интернационализацией. Например в случае с арабской вязью все становится куда веселее в некоторых случаях.


                                  1. vintage
                                    24.07.2015 17:39

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

                                    Продолжение в следующей серии :-)


                                    1. corvette
                                      24.07.2015 18:19

                                      Если бы в следующей, я так понимаю одноразовые биндинги появились в серии 1.3 и их долго ждали.


                                    1. lega
                                      24.07.2015 22:33

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

                                      Любое* большое приложение можно разбить на (независимые) компоненты, где у каждой компоненты свой «rootScope», таким образом можно сделать, что-бы 2000 компонентов с 2000 ватчей в каждом, перепроверялись быстро.
                                      Не знаю как себя поведет Angular.js, но в Angular Light с этим порядок — например разработчики делают меню и другие «оторванные» компоненты изолированными, так же можно подчиненные элементы «изолировать», например комментарии или посты в списке могут быть сами по себе.


                                      1. vintage
                                        24.07.2015 23:28
                                        -1

                                        Класс, давайте ещё костылей :-)


                                    1. Fesor
                                      25.07.2015 01:35

                                      одноразовые биндинги нужны для того что бы устранить необходимость в лишних ватчерах. В доброй половине случае данные которые уже были отрисованы в DOM больше никогда меняться не будут. А значит нет смысла за ними следить.

                                      Собственно если мы пользуемся одноразовыми биндингами нам уже нет дела до того что модель поменяла состояние. 99% что если у нас с этим проблемы, значит что-то пошло не так.

                                      Единственный кейс где это видется проблемой — проекты с интернационализацией. А еще есть случаи и арабской лакализацией, когда смена локали полюбому вызовет полную перерисовку DOM (в силу особенностей RTL), и тогда можно смело использовать bindOnce и просто перезагружать UI.

                                      Или например смена выбранной валюты, тогда надо фильтры поперезапускать или еще чего. На этот кейс одноразовые биндинги никогда расчитаны небыли. Для этого либо мы можем воспользоваться старыми добрыми биндингами и создать кучу ватчеров, или позагоняться и сделать декоратор над $parser который, например. если вы используйте фильтры, аргументы которых могут поменяться, и при этом используете bindOnce, будет заменять это на использование директивы, где будет просто дожидаться события вместо отслеживания изменений через watch. Или вообще запилить свою реализацию labeled watches используя какую-нибудь FRP библиотеку (я правильно понимаю, что только это для вас не кастыль?). Единственное что тогда вам придется явно везде прописывать зависимость данных.


                                      1. vintage
                                        25.07.2015 07:17

                                        В связи с какой особенностью RTL мне придётся перерисовывать весь DOM?

                                        Или вообще запилить свою реализацию labeled watches используя какую-нибудь FRP библиотеку (я правильно понимаю, что только это для вас не кастыль?). Единственное что тогда вам придется явно везде прописывать зависимость данных.
                                        Да. И нет, не придётся.


                                    1. Arkasha
                                      17.08.2015 15:19

                                      Можно поподробее как ручками обновить односторонее связывание?


                                      1. Fesor
                                        17.08.2015 15:28

                                        По сути никак, только перезапустить весь кусок UI который нужно.


                                        1. Arkasha
                                          17.08.2015 21:40

                                          Это можно из коробки как-то сделать или костыли?


                                          1. Fesor
                                            17.08.2015 22:38

                                            конечно кастыли, bindOnce, как это понятно из названия, биндится только единожды. Есть стороння реализация bindOnce, и она позволяет рефрешить их.


                                          1. Drag13 Автор
                                            18.08.2015 09:35

                                            Пишется своя директива со своими правилами рендера.


                    1. DigitalSmile
                      23.07.2015 15:40
                      +1

                      Не очень понял, что мы нарушаем. Данные как устанавливались, так и устанавливаются в одном месте. Решать отображать изменения или нет и в каком виде, это дело вью-модели и шаблона. Обычный MVVM паттерн.

                      Ну а про хрупкость вообще не понял.


                      1. corvette
                        23.07.2015 15:49

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


                        1. Drag13 Автор
                          23.07.2015 15:58
                          +1

                          Пишите разные директивы под разные задачи. Single responsibility еще никто не отменял. Кстати это же решает проблему с хрупкостью директивы.


                          1. corvette
                            23.07.2015 16:20
                            -1

                            Так легко свалиться в copy-paste


                        1. DigitalSmile
                          23.07.2015 16:17
                          +1

                          Итак, вдумаемся в происходящее.
                          Директива — компонент, который представляет собой переиспользуемый виджет или специфичный код для работы с DOM-деревом браузера и стилями.
                          У каждой директивы есть свой шаблон. Если возникает задача, которая не подпадает под ответственность этой директивы (например, в одном месте надо показывать связывание «two-way», в другом «one-way») у нас с Вами есть два пути:

                          1. Делаем новую директиву, если логика трансформируется настолько, что не может быть обслужена существующей директивой.
                          2. Делаем другой шаблон, удовлетворяющий нашей задаче и используем его в зависимости от некой логики (в контексте обсуждения неважно кем и где она будет задаваться, но чаще всего это внутренняя логика директивы)


                          Это дополнительный параметр, причем чем сложнее модель, тем больше таких дополнительных параметров. Это очень плохо.

                          Здесь я с Вами соглашусь, это есть плохо. Но при этом, Вас никто не заставляет так делать и есть куда лучшие микроархитектурные решения.


                          1. corvette
                            23.07.2015 16:21

                            Решить можно, но как-то не изящно это все, поэтому и называется костылем.


                            1. DigitalSmile
                              23.07.2015 16:29

                              А как было бы поизящнее, просто интересен Ваш ход мыслей?


                              1. corvette
                                23.07.2015 16:35

                                Я не знаю изящного решения на Ангуларе, костыль с биндингами вызван проблемами с производительностью. Ваши довольно хорошие, я примерно так и поступаю, когда использую ангулар.


                                1. DigitalSmile
                                  23.07.2015 16:45
                                  +2

                                  Я все равно в упор не пойму, почему односторонний биндинг это костыль?
                                  Давайте монады окрестим костылем, потому что они были придуманы в т.ч. из-за проблем с читаемостью кода с тучей проверок, почему нет?


                                  1. corvette
                                    23.07.2015 16:52

                                    Попробую по-другому. Есть красивая концепция: двусторонний биндинг. И есть костыль: если у вас проблема с производительностью используйте одноразовый биндинг. Это костыль потому, что пользоваться им неудобно, например, в компоненте надо переопределять шаблон. У вас есть 100 компонентов на странице, вы хотите добавить еще один и должны переделать все предыдущие, чтобы заработало.


                                    1. vintage
                                      23.07.2015 18:38

                                      Двусторонний биндинг — это немного о другом :-)


                                      1. corvette
                                        23.07.2015 18:43

                                        Да, погорячился. Речь шла o data-binding.


                                    1. DigitalSmile
                                      23.07.2015 19:06

                                      И есть костыль: если у вас проблема с производительностью используйте одноразовый биндинг.

                                      Вы, мне кажется, немного подменили понятия. Одноразовый биндинг он не для улучшения производительности был придуман (хотя безусловно одна из причин его возникновение это стремление улучшить Ангуляр в этом месте), а для усечения логики работы там, где двунаправленная связь не требуется.
                                      Согласитесь, глупо следить за моделью, которая один раз загрузилась и больше не собирается меняться. Это архитектурно и логически неверно. Поэтому умные ребята сели, почесали репу и поняли что промазали при проектировании Ангуляра с этой фичей. Я, по крайней мерее, себе это так вижу.

                                      Вообще, мне кажется, лучше было бы обращать внимание на другие более слабые места, типа неэффективной работы ng-repeat или отвратительной поддержки локализации, чем на односторонний биндинг и количество теоретически обрабатываемых вотчеров…


                              1. lega
                                23.07.2015 18:48

                                А как было бы поизящнее
                                В Angular Light можно этим рулить — «переключить» декларативный биндинг в одноразовый или сделать двух (N) разовый и т.п.
                                Возможно это изящнее дублирования шаблона или директивы.


                                1. DigitalSmile
                                  23.07.2015 19:09

                                  Спасибо за наводку, не видел до этого этот проект. Ознакомимся…

                                  Возможно это изящнее дублирования шаблона или директивы.

                                  Речь не о дублировании, а о создании шаблона/директивы удовлетворяющим задаче.


                            1. Drag13 Автор
                              23.07.2015 16:35

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


                              1. corvette
                                23.07.2015 16:40

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


        1. DigitalSmile
          22.07.2015 14:32
          +1

          Представьте себе, в Ember тоже есть data-binding, и реализован он в тысячу раз лучше, чем в Angular, как с точки зрения производительности, так и удобства.

          А что касается декларативности, то JS-код в Ember пишется декларативно благодаря в ленивым, кэшируемым вычисляемым свойствам (computed properties). В Angular сплошная императивщина, на которую после Ember смотреть противно.


          Сравнить не могу, потому что Ember не использовал. С удовольствием прочитаю Вашу статью на эту тему.
          А чем кстати «императивщина» вдруг стала хуже декларативного подхода? По моему просто два разных подхода, со своими плюсами и минусами.

          Это самый большой п#@дёж, который можно услышать в пользу Angular.

          В реальных проектах вы сталкиваетесь с HTML-элементами, имеющих десятки атрибутов.


          Если у Вас десятки атрибутов на одном html-элементе, это сигнал о том, что пора бы эту директиву разбить или изменить интерфейс доступа к ней. Никогда честно говоря не испытывал с этим сложности.

          Вот за что приходится платить, так это за Angular'овский dirty-checking. Сделать на Angular тяжелый, сложный вэб-интерфейс просто невозможно. Чтобы справиться с тормозами, приходится отказываться от data-binding'а и обновлять DOM по-старинке.


          Не соглашусь, эта задача вполне Ангуляру по силам.


          1. lolmaus
            22.07.2015 14:48

            А чем кстати «императивщина» вдруг стала хуже декларативного подхода? По моему просто два разных подхода, со своими плюсами и минусами.

            Декларативный подход просто удобнее и приятнее в использовании. Плюс количество кода сокращается (то есть его экспрессивность растет), правда, это заслуга уже Ember, а не декларативщины как таковой.

            Не соглашусь, эта задача вполне Ангуляру по силам.

            По производительности ответил выше.


        1. Apx
          23.07.2015 15:41
          +1

          Сколько хейта в одном комментарии.
          А если по существу то ангуляр ничем не хуже эмбера, просто надо писать какие-то тяжёлые вещи включая периодически мозг. В эмбере например поддержка скобок вобще идет отдельной либой на 150 КБ. Конечно ангуляр тоже не сахар ведь все биндинги проверяются с очень маленьким таймаутом, но зато всё в коробке сразу.

          Понять где директива а где класс или еще что-то можно абсолютно спокойно. Во первых поддержка в иде. Во вторых можно просто для себя ввести кодстайл и не париться. Так что аргумент про поиски класса в элементах считаю более чем неконструктивным. Сейчас вот проект в котором все поля объектов динамические. Я с генерацией всего этого дела на странице пока что максимум в 400 вотчеров впаялся. Это при наличии 60 полей у объекта и редактируемой привязанной таблицей. Ни от каких байндингов при этом не отказываюсь. Везде ng-model с ng-messages и прочие кастомные директивы. Все бегает, даже на смартфоне (что на самом деле удивило). Яичницу надо делать на умеренном огне, и просто следить.


    1. symbix
      22.07.2015 13:16

      > Angular зациклился и упал

      Ну дык, === же. Эффективный способ — когда в модели один раз создан объект и он модифицируется.

      > Мое вольное сравнение Angular…

      Да, вы правы в том, что Angular — довольно низкоуровневый, и без постройки поверх него более высокоуровнего фреймворка, где действительно будет реализована хотя бы буква M из известных аббревиатур, что-то сложное написать нереально. Примерно как в PHP, да.


    1. aav
      22.07.2015 16:15

      Я столкнулся с известной проблемой, что примитивы по scope'ам не передаются, а копируются.

      Что, куда и когда копируется?


      1. lolmaus
        22.07.2015 16:20

        Свойства родительского scope в дочерний. Свойства-объекты передаются по ссылке, а свойства-примитивы копируются по значению, теряя связь с родительским scope.

        github.com/angular/angular.js/wiki/Understanding-Scopes


        1. aav
          22.07.2015 16:28

          Где вы там копирование нашли? Здесь?

          Scope inheritance is normally straightforward, and you often don't even need to know it is happening… until you try 2-way data binding (i.e., form elements, ng-model) to a primitive (e.g., number, string, boolean) defined on the parent scope from inside the child scope. It doesn't work the way most people expect it should work. What happens is that the child scope gets its own property that hides/shadows the parent property of the same name.


          1. lolmaus
            22.07.2015 16:29

            Копирование примитивов по значению я нашел в JavaScript.


            1. aav
              22.07.2015 16:31

              Но ссылку почему-то даете не на Javascript, потому немного сложно понять, что вы вообще имеете в виду.


              1. lolmaus
                22.07.2015 16:33

                По моей ссылке говорится дословно следующее:

                If item is a primitive (...), essentially a copy of the value is assigned to the new child scope property.


                1. aav
                  22.07.2015 16:44

                  Вы чересчур обобщили. Эта фраза имеет смысл в контексте ng-repeat, а не в целом по отношению к scope-иерархиям. И проблемой обычно является в связке с ng-model. Как там в примере и приведено.
                  Но ваш изначальный пример был про getter-свойство. Вы там память что ли экономили?


                  1. lolmaus
                    22.07.2015 16:50

                    Эта фраза истинна в целом по отношению к scope-иерархиям.

                    Вот, сделал вам пруф: plnkr.co/edit/n1Xwfc76rK0CDeHXBrcM?p=preview


                    1. aav
                      22.07.2015 16:57

                      Нет, не истинна. Вот вам пруф plnkr.co/edit/VGLBaQbCsMciJvcHzKWz?p=preview — посмотрите в консоли. Просто scope прототипно наследуются.


                      1. lolmaus
                        22.07.2015 17:15

                        Пардон, я написал <input value="{{name}}"> вместо <input ng-model="name">

                        plnkr.co/edit/zKXcV9ysz899QYNCaXqz?p=preview

                        Имените содержимое текстового поля и загляните в $scope в консоли.

                        И дело не ограничивается ng-model. Вот аналогичная ситуация вообще без директив:

                        plnkr.co/edit/3eYhxJSaBiAsQ2jGs88N?p=preview


                        1. aav
                          22.07.2015 17:23

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

                          P.S. все также непонятно, какое это отношение имеет к getter-свойству?..


                          1. lolmaus
                            22.07.2015 17:56

                            Нет, дело не так просто, как вы говорите. Загуглите angular scope primitives, вы найдете десятки статей, в которых говорится, что ноги у этой проблемы растут из того факта, что объекты в JS передаются по ссылке, а примитивы — по значению, в результате чего создается дубликат примитива.

                            Но называйте как хотите. В Ember (как и, думаю, любом другом фрэймворке, кроме пресловутого Angular) data binding — это data-binding. Он устанавливается независимо от того, написали вы {{foo}} или {{foo.bar.baz}}.

                            Только в Angular может быть зашито два разных поведения в зависимости от наличия точки в пути к свойству. Только в Angular три разных поведения устанавливаются закорючками @, % и &. Только в Angular документация по сервисам содержит информацию о .factory() и не содержит о .service(), а инфа по .service() находится в документации по провайдерам. Только в Angular можно убить час в тщетных попытках найти документацию по хуку link, который используется почти в каждой директиве (I dare you). Только Angular имеет ограничение на 2000 одновременных binding'ов. Только Angular пересчитывает всё по тысяче раз в минуту безо всякой на то нужды.

                            > P.S. все также непонятно, какое это отношение имеет к getter-свойству?..

                            В директиве на $scope было объявлено getter-свойство, которое вычисляло строку, можете вы себе такое представить? При передаче этого свойства в изолированный scope оно переставало обновляться. То есть в родительском scope оно пересчитывалось, а в изолированном всегда отображалось первоначальное значение. Очевидно, это происходит из-за того, что при передаче примитива в дочерний scope он копируется, а не передается по ссылке, что вы зачем-то пытаетесь отрицать.


                            1. aav
                              22.07.2015 18:03

                              Я не знаю, зачем вы все в кучу начали валить.

                              Я не просто отрицаю, я вам и пруф дал на основе вашего же плунка. Ваша изначальное обобщенное высказывание некорректно. Не вводите людей в заблуждение. Не каждая статья в интернете — это истина в последней инстанции. Максимум — это повод углубить и расширить свое понимание о работе фреймворка, а не зазубрить мифы.


                              1. lolmaus
                                22.07.2015 18:16

                                Ваша изначальное обобщенное высказывание некорректно.

                                Я забыл в изначальном высказывании слово «изолированный». Извините, пожалуйста.

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


                                1. aav
                                  22.07.2015 20:02

                                  Разница в понимании/непонимании происходящего. А потом трансляции заблуждений другим людям. С мифами надо бороться, по мере возможностей.

                                  plnkr.co/edit/XuFKYjo9Wxi9aPKO7dQk?p=preview
                                  Что там у вас не обновлялось?


                                  1. lolmaus
                                    23.07.2015 17:26

                                    Ну что-то там еще вмешалось значит. Воспроизвести зацикленность на Plunkr мне тоже не удалось. Реальный проект устроен намного сложнее, чем изолированный пример.

                                    Факт, что проблема необновления свойства внутри изолированного scope имела место, и она решилась оборачиванием свойства в объект. Только делать это пришлось вне геттера.


                            1. DigitalSmile
                              22.07.2015 18:19

                              Haters gonna hate…
                              Не знаю, что еще можно Вам написать. :)


                              1. lolmaus
                                22.07.2015 18:20

                                А вы попробуйте Ember. Освойте его хотя бы на базовом уровне, чтобы почувствовать вкус. И тогда поговорим.


                                1. DigitalSmile
                                  22.07.2015 18:24

                                  Обязательно попробую как будет подходящий проект для экспериментов. Может как раз у Вас к этому времени созреет статья-сравнение.


    1. Fesor
      23.07.2015 17:42

      Я столкнулся с известной проблемой, что примитивы по scope'ам не передаются

      ибо нефиг передавать данные напрямую через скоуп. Вообще хорошей мыслью будет ограничить себя в том где можно использовать скоуп — в link директивы и только там.

      Для всего остального — controller as синтаксис, тогда этих проблем не возникает а у разработчиков перестают чесаться руки делать ватчи лишние.


  1. aav
    22.07.2015 16:13

    За этим, казалось бы, маленьким пунктом на последнем месте:

    Пользуйтесь параметром track by в директиве ng-repeat. Во первых это быстрее, а во вторых убережет от ошибки duplicates in a repeater are not allowed, которая возникает, когда мы пытаемся вывести одинаковые объекты в списке.

    скрывается чуть ли не самая распространенная проблема тормозов AngularJS.

    Упереться в количество биндингов — это еще постараться надо.
    А вот пересоздавать изрядный кусок DOM на каждый чих у людей получается достаточно часто, чем многие бенчмарки от людей, не очень знакомых с AngularJS, и страдают.

    Подробнее в этих ветках:
    habrahabr.ru/post/246905/#comment_8207725
    habrahabr.ru/post/246905/#comment_8262489


    1. Drag13 Автор
      22.07.2015 18:18

      Наверное стоило бы расписать эту особенность, но и так статья получилась слишком большая.
      П.С. Как писали в одной из статей ng-repeat — ваш самый плохой друг. Поэтому работать с ним желательно осторожненько.


  1. ilyak
    22.07.2015 17:29
    -3

    Если кратко: избегайте использования AngularJS


    1. Drag13 Автор
      22.07.2015 17:47

      А еще не используйте автомобили (там аварии), компьютеры (от них зрение портится) и не дай бог вам спортом заниматься (он здоровье калечит).
      Все надо делать с умом и будет всем счастье.


    1. corvette
      23.07.2015 09:30
      -1

      Да не много вы вынесли из статьи. Может быть у вас предвзятое отношение к Angular?


      1. ilyak
        23.07.2015 11:53

        Это был скорее шуточный комментарий. Просто слишком часто в статье встречаются фразы типа «избегайте использовать такую-то крутую возможность ангуляра».
        А статья полезная, я как раз столкнулся с проблемами производительности при создании своего первого проекта на ангуляре.


        1. Drag13 Автор
          23.07.2015 12:42

          За силу всегда приходится платить.
          Рефрен статьи должен был быть примерно таким:
          «Избегайте бездумно использовать такую-то крутую возможность ангуляра»


          1. symbix
            24.07.2015 01:24

            Избегайте бездумно использовать [что угодно] :-)