Информация

Дата основания
Местоположение
Россия
Сайт
www.yandex.ru
Численность
свыше 10 000 человек
Дата регистрации

Блог на Хабре

Обновить
Комментарии 29

Постоянно сталкиваюсь, что кто-то использует => вместо bind.
Это прямо как болячка какая-то. И одно дело если экземпляров класса будет 1–10, а другое когда их сильно больше.

Самый очевидный минус: каждый конструктор тратит время на создание этой новой функции.

Бинд тоже создает новый экземпляр функции что легко проверить:


function myFunc(){};
var binded = myFunc.bind(window);
binded === myFunc; // false

И точно так же перезатирает предыдущее значение. Поэтому не факт что код будет оптимизирован.

Да, вы правы. bind, конечно создает новую функцию с зафиксированным контекстом, но он скорее всего будет быстрей, потому что во внутренностях движка есть возможность сослаться на уже существующую в прототипе функцию.
Даже решил немного измерить в попугаях. В Хроме и Файрфоксе bind победил. В Сафари, на удивление, победил вариант со свойствами-стрелками.
Для Ноды тоже написал небольшой тест. Там bind победил вообще с огромным разрывом по скорости и чуть лучшим расходом памяти.


И опять вы правы, значение перетрется.
Но в случае с bind вместо стрелок мы все еще можем получить в наследнике исходный метод через super, а со стрелками не можем, вместо этого словим исключение TypeError: (intermediate value).какаяТоФункция is not a function


Псевдопример с обращением к super
class A {
  constructor() {
    this.hi = this.hi.bind(this);
  }

  hi() {
    console.log('A');
  }
}

class B extends A {
  constructor() {
    super();

    super.hi();
    this.hi();
  }

  hi() {
    console.log('B');
  }
}

class C {
  hi = () => {
    console.log('C');
  }
}

class D extends C {
  constructor() {
    super();

    super.hi();
    this.hi();
  }

  hi() {
    console.log('D');
  }
}

new B();
new D();

Т.е. для компонентов которые создаются однократно (app.tsx, landing.tsx userinfo.tsx e.t.c) выгоднее* использовать стрелочную функцию, потому что:


наследование не предполагается, а создание одной функции быстрее чем создание ее же + создание враппера с контектстом (метод bind)


*С чисто математической точки зрении т.к. разница будет просто ничтожна, даже на устаревших мобильных (интересно было бы померять). Правда справедливо и в обратную сторону — если вы не создаете 100+ экземпляра компонента вам должно быть все равно с точки зрения CPU. Да, и с точки зрения памяти тоже.


Т.е. мне кажется совет актуален больше для библиотек.


Спасибо за замеры и за статью, есть над чем подумать.

Полагаю в рамках любого React приложения вы говорите уже даже не о спичках, на фоне пожара, а о булавках. Первый же {...props} перетрёт все ваши старания. А первый же хук просто уничтожит.

Все не так однозначно. При 3x bind класс начинает проигрывать 3x arrow в chome. К тому же, не знаю как для кого, но для меня писать с bind, особенно большие классы, небезопасно (можно пропустить/забыть сделать bind). Не люблю в общем.

По поводу наследования, в моей практике, как правило обработчики и публичные методы не переопределются при наследовании. Но если понадобится, придется делать bind, да

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


Надо вынести определения классов в сетап бенчмарка и измерять или создание экземпляра класса + вызов метода (сценарий "создаём короткоживущий экземпляр, используем один раз и выбрасываем") или только вызов ("долгоживущий экземпляр, вызываем много раз")


  1. Создание + один вызов метода: https://jsbench.me/6nkjv4vq9u/1
  2. Вызов метода: https://jsbench.me/6nkjv4vq9u/2

Теперь разница в скорости становится заметнее.

Запустил. У меня даже двукратной разницы не было. Мне кажется любые попытки оптимизации React приложений на этом уровне это бессмысленная трата времени. Потому что если вы и правда хотите что-то на этом выиграть, то другие оптимизации (вроде inline-а компонент, отказа от spread-а, или вообще смены технологии) дадут вам несоизмеримо больше выгоды.

Бинд тоже создает новый экземпляр функции… Поэтому не факт что код будет оптимизирован

Bind создаёт "обёртку" вокруг уже существующей функции, то есть N вызовов N "обёрток" всё равно придут выполняться в один экземпляр исходной функции. Она будет вызвана N раз, соберёт N раз информацию о типах, с которыми вызывалась, и если N больше порога (ЕМНИП 10000 по умолчанию в V8 и JavaScriptCore), то функция будет оптимизирована.


То, во что превращается arrow method после транспиляции в ES6- (и скорее всего то, что под капотом выполняется в нативно поддерживающих arrow method движках), создаёт N экземпляров независимых друг от друга функций. N вызовов N экземпляров приведут к тому, что каждый экземпляр функции будет вызван только один раз. Оптимизатор к N функциям, вызванным по одному разу, не проявит интереса.

А. Ну т.е. речь идёт больше не о том, что быстрее, а о том, что получит JIT раньше? Ну да, в таком разрезе это имеет больше смысла. Но всё равно кажется что вы занимаетесь чем-то очень странным. К примеру если вы перейдёте на хуки (что имхо неизбежно учитывая политику React), то там вообще всё на замыканиях

В существующих проектах всё ещё много классовых компонентов, и их надо сопровождать. Разработчики часто пытаются в них заменить bind на стрелочные методы. Если можно не допустить ухудшения производительности в этом месте простым требованием к стилю кода, то почему бы и нет?


Ну и кроме классовых компонентов React могут существовать классы в модели данных. Там возможна аналогичная ситуация.

Если можно не допустить ухудшения производительности в этом месте простым требованием к стилю кода, то почему бы и нет?

Это самое интересное. Тут мы переходим на скользую почву субъективных оценок. На мой взгляд потери от constructor + bind сильно выше преимуществ в производительности. В виду того, что код с => сильно проще в написании и поддержке. Куда меньше визуального мусора. А это "условно" можно перевести в ресурсы бизнеса — деньги и время.


Но я не смогу не согласиться, что это мнение субъективное и кому-то может показаться, что никакой особой разницы нет. Ведь кому-то и Java код не кажется too verbose.

Фокус с .bind не прокатит, будет возвращен новый экземпляр и бла-бла, вы уже это поняли я вижу.
Есть только один вариант устраняющий этот недостаток, это чисто на уровне трансформации итогового кода в бандле преобразовывать
myClass.sayHi('Ivan', 'Petrov');

в
myClass.sayHi.call(myClass, 'Ivan', 'Petrov');

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

Какой метод? В какой обработчик? Код напишите.
Я то вот про это:
class MyClass {
   asd = 1;
   sayHi(){
        console.log('asd:', this.asd);
   }
}

const myClass = new MyClass();

setTimeout(myClass.sayHi.call(myClass), 0); // asd: 1
setTimeout(myClass.sayHi, 0); // asd: undefined

Ну так вы и привели код который не будет работать.


setTimeout(myClass.sayHi.call(myClass), 0);

Вы пытаетесь undefined передать как обработчик для setTimeout.


Метод sayHi объекта myClass вызывается сразу, а потом его результат вы зачем-то передаете в setTimeout как колбэк.
Попробуйте поставить таймаут на 1000 миллисекунд, и поймете о чем я говорю.

Ааа, ну тут да, просто функция сразу вызывается и возвращает undefined таймауту для выполнения, вместо ссылки на функцию. Такие функции в качестве аргумента не передашь да. Ну значит увы и ах. И такой метод не катит. Недостатки JS, увы.
class A extends PureComponent {
   a = 1;
   b() { console.log(this.a); }
   render() {
    return <button onClick={this.b}>click</button>
   }
}

Что должен здесь сделать трасформер кода?


return <button onClick={this.b.call(this)}>click</button>

Очевидно нет. А что тогда?

Какая альтернатива? Я не знаю способа который бы не подождал по функции на инстанс и мог привязать контекст к колбэку.


Проблема наследования решается точечно и на моей практике возникает очень редко как и описанная проблема тестирования.

Каждый конструктор тратит время на создание новой функции

Я бы убрал из минусов. Просто потому, что выбора у вас всё равно нет. Дело не в =>, а в том что не существует никакого способа привязать this к методу без создания новой функции. Используете класс-компоненты — используете ту или иную версию .bind, будь то =>, .bind, декораторы, whatever.


Да и, вспоминая хуки, мы эти лишние функции создаём уже, ну просто, в промышленных масштабах. Из-за замыканий и хуков.


Касательно наследования — мне кажется React и наследование столь сильно несовместимы, что любые попытки с этим воевать — зря. Особенно теперь, когда это по сути legacy.


Хотя разработчики React и хуков явно говорят в документации, что функции, которые возвращаются из useState и из useReducer, не меняются при ререндерах

Небольшой нюанс. Во время рендера к вам вместо setState из useState может, совершенно внезапно, прилететь () => {}. Да-да. Не удивляйтесь. Это react dev tools. Они там древо хуков в своей панели составляют. А теперь представьте себе такой код:


const [st, setSt] = useState(whatever);
const ref = useRef(setSt);
ref.current = setSt;

Если у вас в кодовой базе есть такой код, то у меня для вас плохие новости ;) Впрочем про то, почему нельзя мутировать или обращаться к ref value во время рендера можно целую статью сварганить.


Но если код получается слишком сложный, то React не понимает, что мы от него хотим, и просто монтирует новое дерево взамен старого

Вот это самое интересное. Но увы только "слишком сложный". Полез в код — там муть, надо разбираться в "что такое IndeterminateComponent". Если React может позволить себе на ровном месте выкинуть произвольный компонент из древа и создать его заного, то это чревато куда большими проблемами, чем просто performance.

Каждый конструктор тратит время на создание новой функции
Я бы убрал из минусов.

Всё-таки в V8 и нативный arrow method, и создание в конструкторе новой функции с нуля this.method = () => {} пока что медленнее, чем bind уже существующей в прототипе функции. V8 мне важен потому, что на нём работает большинство аудитории наших проектов.


https://jsbench.me/6nkjv4vq9u/1


Скорость выполнения, миллионов операций в секунду (больше — лучше):


Browser         Native =>    Transpiled =>    Bind
Chrome 87       1.8          24               55
ChromeMobile 87 0.18         2.7              4.5
Firefox 84      12           12               8
Safari 14       1.7          19               11

Чтобы лучше прочувствовать эти числа, можно использовать такой подход:


В ChromeMobile использование нативного arrow method означает лишние 5-6 микросекунд на создание экземпляра класса. Тысяча экземпляров — это уже 5-6 миллисекунд только на их конструирование, и это уже близко к бюджету в 16 миллисекунд на кадр при 60fps. Получаем ограничение сверху, которое ближе, чем хотелось бы, к реальным количествам экземпляров в реальных приложениях.

Тысяча экземпляров — это уже 5-6 миллисекунд только на их конструирование

Тут скорее вопрос какова доля этих 5-6 микросекунд в общей инициализации проекта? Даже пусть в пределах кадра. Если стоит задача экономии на таком уровне, не проще взять условный Svelte? Или перестроить UI так, чтобы речь не шла о таких нюансах и можно было заняться бизнес-логикой :)

Во время рендера к вам вместо setState из useState может, совершенно внезапно, прилететь () => {}. Да-да. Не удивляйтесь. Это react dev tools. Они там древо хуков в своей панели составляют.

А вы могли бы на это завести баг в React Dev Tools?


Но увы только "слишком сложный". Полез в код — там муть, надо разбираться в "что такое IndeterminateComponent".

Я это увидел при работе с компонентом, завёрнутым в HOC, а внутри HOC использовался хук. И при переносе кусочков кода между HOC и хуком возникал этот эффект. Хорошо, что были unit-тесты, которые сразу же словили проблему с монтированием вместо апдейта. Плохо, что не получилось выделить минимальный кусок кода, в котором бы это воспроизводилось — всё слишком сложно, и времени не хватает, как обычно.


Кстати, если бы все разработчики писали для всех своих компонентов тесты на изменение пропсов и ререндер с проверкой, что произошёл именно апдейт, а не монтирование нового дерева, то мы бы скорее всего слышали об этой проблеме чаще.

А вы могли бы на это завести баг в React Dev Tools?

Оказалось, что это не баг :) It works as planned. И даже, за флагом, есть специальный валидатор.


Я это увидел при работе с компонентом, завёрнутым в HOC, а внутри HOC использовался хук. И при переносе кусочков кода между HOC и хуком возникал этот эффект.

Я надеюсь, что дело было всё таки в чём-то другом. Отбрасывать лишние рендеры ещё куда не шло, но демонтировать компоненты просто так это уже какая-то дичь :-(

Оказалось, что это не баг :) It works as planned. И даже, за флагом, есть специальный валидатор.

То есть, правила работы с ref теперь гласят:


  • чтение и запись ref.current — это сайд-эффекты, поэтому нельзя безопасно перезаписывать и даже просто читать ref.current при рендере
  • можно сохранять что-то в начальном значении ref
  • можно читать/писать ref.current внутри useEffect()

Если в коде из упомянутого бага запоминать setState так, как требуют правила, то React DevTools не ломают работу нашего кода


  const [state, setState] = useState("A");

  // так нельзя
  // const setStateRef = useRef();
  // setStateRef.current = setState;

  // так можно
  const setStateRef = useRef(setState);

Если всё сформулировать так, то я не вижу ничего страшного. Да, React DevTools подменяют useState и подсовывают свой setState, чтобы узнать, как устроены наши хуки, но делают это в соответствии с правилами. И если мы работаем с ref и с полученным setState тоже по правилам, то наш код не сломается от включения DevTools, и мы всё так же можем применять принцип "запомнить первый setState и не перегенерировать зависящие от него функции".

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


При том что года 2 назад сервисом можно было пользоваться.

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

Простите, но ваш код — сферический в вакууме. Есть какие-то замеры в начале, а дальше ожидал больше цифр от цифровой компании.


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

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