Всем привет! Я джуниор фронтенд разработчик. И хотел бы рассказать как иногда применение библиотек в проекте - это излишества, которые стоит избегать.

Давай начнем по порядку. Проект написан на NextJs, TS, TailwindCSS. И есть на сайте анимационная кнопка, которые при скролле красиво появляется и при клике открывает модалку.

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

НО. Проблема была в том, что иногда анимация подвисала, дергалась, а на слабых ПК вообще отказывалась работать.

В итоге решился взяться за анимационную кнопку и посмотреть что же там ее кишках :-)
И, о ЧУДО! Если посмотреть на компонент, то мне кажется он очень страшным. Как вам кажется?

export const AnimatedButtonLinkWitmUtm: React.FC<IAnimatedButtonProps> = ({
  isDark = false,
  btnColor = '#0A85D1',
  btnShadow = '0 0 0 0px rgb(157,52,218)',
  maxWidth = 315,
}) => {
  const buttonRef = useRef<HTMLDivElement>();
  const tl = gsap.timeline();

  useEffect(() => {
    if (buttonRef.current) {
      const trigger = ScrollTrigger.create({
        trigger: buttonRef.current,
        start: `top bottom-=400px`,
        onEnter: () => {
          tl.to(buttonRef.current, { opacity: 1, duration: 0.3 })
            .to(buttonRef.current, {
              duration: 0.3,
              boxShadow: btnShadow,
              ease: 'circ.in',
            })
            .to(buttonRef.current, {
              duration: 0.3,
              boxShadow: btnShadow,
              ease: 'circ.out',
            })
            .to(buttonRef.current, {
              maxWidth: `${maxWidth}px`,
              width: '100%',
              paddingLeft: 24,
              duration: 1,
            })
            .to(buttonRef.current.children[1], { opacity: 1, duration: 2 }, '<0.4')
            .to(buttonRef.current.children[0], { opacity: 1, duration: 2 }, '<0.5');
        },
        onLeaveBack: () => {
          tl.to(buttonRef.current, { opacity: 0 })
            .to(buttonRef.current.children[1], { opacity: 0 })
            .to(buttonRef.current, { width: '55px', paddingLeft: 10 })
            .to(buttonRef.current.children[0], { opacity: 0, delay: 0, duration: 0 });
        },
      });

      return () => {
        trigger.kill();
      };
    }
  }, [btnShadow, maxWidth, tl]);

  return (
    <>
      <div
        ref={buttonRef}
        className={cn(
          'relative flex h-[56px] w-[56px] items-center justify-between gap-[16px] rounded-[10px] px-[10px] py-[8px] opacity-0 shadow-[inset_0px_1px_0px_0px_rgba(0,0,0,0.11)] backdrop-blur-[3.5px]',
          {
            'bg-[#D8D8D8]/30': !isDark,
            'bg-[#424245]/70': isDark,
          }
        )}
      >
        <LinkWithUtm
          href="https://a6b9d8dc-b142-4b92-b1d0-dfbfd2230471.selstorage.ru/assets/edu-program.pdf"
          target="_blank"
          className={cn(
            'm-0 text-[17px] font-medium leading-[27.2px] opacity-0 after:absolute after:inset-0',
            {
              'text-black': !isDark,
              'text-[#F5F5F7]': isDark,
            }
          )}
        >
          Подробнее о программе
        </LinkWithUtm>
        <div
          className="h-[40px] w-[40px] min-w-[40px] rounded-[10px] p-[8px] opacity-0"
          style={{ backgroundColor: btnColor }}
        >
          <Image src={arrowRight} alt="" />
        </div>
      </div>
    </>
  );
};

У нас есть логика на GSAP и триггер на каком моменте скрола она должна появляться.
Если посмотреть на анимацию, она основана на размерах и opacity.

А нужен ли нам GSAP для такой простой анимации??? НЕТ. И давайте я постараюсь объяснить почему.
1 - Вес библиотеки. Ради простейшей анимации вы подтягиваете в проект библиотеку весом 4 мб.
2 - Эту анимацию можно сделать на чистом CSS

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

React-Intersection-Observer
React-Intersection-Observer

Как вы видите пакет намного легче и популярнее по скачиваниям, так как он перекрывает простые потребности по триггерам и анимациям :-)

И что в итоге у меня получилось

export const NewAnimatedButtonWithLinkUtm = ({
  isDark = false,
  btnColor = '#0A85D1',
}: IAnimatedButtonProps) => {
  const { ref, inView } = useInView();

  return (
    <div
      ref={ref}
      className={twMerge(
        'sticky bottom-8 top-[80vh] mx-auto mt-8 flex h-[56px] cursor-pointer items-center justify-between gap-[16px] rounded-[10px] px-4 py-[8px] shadow-md backdrop-blur-[3.5px] transition-all',
        isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',
        inView ? `opacity-1 w-[315px] delay-500 duration-1000` : 'w-[65px] opacity-0'
      )}
    >
      <LinkWithUtm
        href={linkHref}
        className={twJoin(
          'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',
          isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',
          inView ? 'opacity-1 delay-1000 duration-1000' : 'opacity-0'
        )}
      >
        Начать учиться бесплатно
      </LinkWithUtm>
      <div
        className={twJoin(
          `absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] p-2 transition-all bg-[${btnColor}]`,
          inView ? 'opacity-1' : 'opacity-0'
        )}
      >
        <Image src={arrowRight} alt="" />
      </div>
    </div>
  );
};

Как вы видите вместо 85 строк кода и сложной логикой, у меня 36 строк кода и стили применяется через логическое значение. Думаю будет намного проще рефакторить такой код :-)

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

Я создал контейнер в котором лежит кнопка и через children принимает секцию, по которой будет скролит пользователь и вот что получилось :-)

export const SectionNewAnimatedButtonWithLinkUtm = ({
  isDark = false,
  btnClassName,
  children,
  className = 'relative mx-auto mb-40 max-w-[1024px] px-5 lg:px-0',
  text = 'Начать учиться бесплатно',
}: IAnimatedButtonProps) => {
  const { ref, inView } = useInView({
    threshold: 0.2,
  });

  return (
    <section ref={ref} className={className}>
      {children}
      <div
        className={twMerge(
          'sticky bottom-8 top-[80vh] mx-auto mt-8 flex h-[56px] cursor-pointer items-center justify-between gap-[16px] rounded-[10px] px-4 py-[8px] shadow-md backdrop-blur-[3.5px] transition-all',
          isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',
          inView
            ? `opacity-1 w-[315px] delay-500 duration-1000`
            : 'w-[65px] opacity-0 duration-1000'
        )}
      >
        <LinkWithUtm
          href={linkHref}
          className={twJoin(
            'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',
            isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',
            inView ? 'opacity-1 delay-1000 duration-200' : 'opacity-0'
          )}
        >
          {text}
        </LinkWithUtm>
        <div
          className={twMerge(
            `absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] bg-[#0A85D1] p-2 transition-all`,
            `${btnClassName}`,
            inView ? 'opacity-1' : 'opacity-0'
          )}
        >
          <Image src={arrowRight} alt="" />
        </div>
      </div>
    </section>
  );
};

Прибавилось 10 строк кода, но ничего страшного. Код все также остается понятным :-)

Вроде бы все, можно открывать ПР и ждать ответа от Тимлида и мержить в основную ветку.

НОООО... Мы можем еще прокачать свой компонент и соблюдать один из принципов ООП - Open/Closed и спрятать всю логику под капот, чтобы фронтенд разработчик в команде пользовался компонентом и ему не приходилось залазить в кишки.

И тут решил я связать контейнер и компонент кнопки через обычный контекст в реакте :-)
Впервую очередь мы создаем контекст и контейнер и передаем основное значение для анимации

export const AnimatedButtonContext = createContext<IAnimatedButtonContext>(null);

export const SectionAnimatedButton = ({ children }) => {
  const { ref, inView } = useInView({
    threshold: 0.2,
  });

  return (
    <AnimatedButtonContext.Provider value={{ inView }}>
      <section className="relative mx-auto mb-40 max-w-[1024px] px-5 lg:px-0" ref={ref}>
        {children}
        <AnimatedBtn />
      </section>
    </AnimatedButtonContext.Provider>
  );
};

Затем в самой кнопке и используем хук useContext и привязываемся к контексту

export const AnimatedBtn = ({
  isDark = false,
  btnClassName,
  text = 'Начать учиться бесплатно',
  link = '',
}: IAnimatedButtonProps) => {
  const { inView } = useContext<IAnimatedButtonContext>(AnimatedButtonContext);

  return (
    <div
      className={twMerge(
        'sticky bottom-8 top-[80vh] mx-auto mt-8 flex h-[56px] cursor-pointer items-center justify-between gap-[16px] rounded-[10px] px-4 py-[8px] shadow-md backdrop-blur-[3.5px] transition-all',
        isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',
        inView ? `opacity-1 w-[315px] delay-500 duration-1000` : 'w-[65px] opacity-0 duration-1000'
      )}
    >
      <LinkWithUtm
        href={link}
        className={twJoin(
          'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',
          isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',
          inView ? 'opacity-1 delay-1000 duration-200' : 'opacity-0'
        )}
      >
        {text}
      </LinkWithUtm>
      <div
        className={twMerge(
          `absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] bg-[#0A85D1] p-2 transition-all`,
          `${btnClassName}`,
          inView ? 'opacity-1' : 'opacity-0'
        )}
      >
        <Image src={arrowRight} alt="" />
      </div>
    </div>
  );
};

Теперь компонент ГОТОВ :-)

Что в итоге этой работы мы добились:
1 - Убрали тяжелую библиотеку
2 - Сделаю логику кнопки проще и следовательно рефакторить станет гораздо проще
3 - Спрятали логику и создали контейнер, в который мы заворачиваем наш контент и все работает :-)
4 - Анимация стала работать гладко и без зависаний, в том числе и на слабых ПК


Статья получилась немного сумбурная, но этим хотел сказать, что применение библиотек не всегда хорошо и иногда полезно залезть в ту самую БАЗУ и посмотреть простое решение :-)

Спасибо всем кто дочитал! Был рад рассказать про парт-тайм проект, на котором работаю и поэтому хочу сказать что я открыт для предложений к офферам и если вам нужен активный и амбициозный разработчик - буду рад с вами пообщаться и пофлексить вашим кодом :-)
https://t.me/sadbatya

А с вами был Владимир! Хорошего дня, вечера и ночи :-)

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


  1. sfi0zy
    17.01.2025 07:04

    Проблема была в том, что иногда анимация подвисала, дергалась, а на слабых ПК вообще отказывалась работать

    Так, погоди-ка... Что-то здесь не так... В изначальном коде анимируется box shadow и padding, что потенциально очень плохо для производительности, а в конечном коде этого нет. Или мне Tailwind глаза съел и я этого не вижу. Кажется, что вы изменили суть анимации, но выдаете это за эффект от убирания GSAP. Живой пример до/после в студию!

    Теперь компонент ГОТОВ :-)

    Да, осталось выкинуть 'sticky bottom-8 top-[80vh] mx-auto mt-8 flex h-[56px] cursor-pointer items-center justify-between gap-[16px] rounded-[10px] px-4 py-[8px] shadow-md backdrop-blur-[3.5px] transition-all bg-[#424245]/70 opacity-1 w-[315px] delay-500 duration-1000', и вообще заживем. А потом мы вспомним былые времена, когда статичные кнопки, у которых даже текст не меняется, делались в виде отдельных чистых HTML, CSS и JS, безо всяких зависимостей, но это уже давно забытые технологии, это мы вряд ли осилим...


  1. SergeiZababurin
    17.01.2025 07:04

    чтобы все срабатывало нам нужен лишь Observer


    Для одной кнопки Observer ? А это не слишком для одной кнопки ? ))))

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


  1. JBFW
    17.01.2025 07:04

    К слову, понадобилось как-то запустить Винду в эмуляторе. Ну казалось бы, какие проблемы, сколько-то памяти, 1 процессор, программа работает.

    А вот браузер уже не работает, не справляется он со страницами, забитыми джаваскриптом. Нужно минимум 2 ядра чтобы хоть как-то шевелилось.

    Это не к автору статьи претензии )

    Поубирали загрузку с бекенда, рендеринг страниц, переложили всё на юзеров, "пусть у них тормозит".

    Бизнес-логика прослеживается, "пусть юзер перелистает 30% контента", но юзер может и болт забить на сайт который тормозит и кнопок не показывает сходу. Верните простой HTML! )