Вміння вирішувати проблеми — твій шлях від «виконавця» до «творця»
Вітаю, друзі, моє імʼя Павлин, і я працюю у компанії Valtech на позиції Solution Architect. Раніше я вже розповідав, як пройшов шлях від Front-end розробника до Solution Architect, почитати можна тут.
Сьогодні я хочу поговорити про problem-solving, та як це важливо для карʼєрного та професійного зростання. Спочатку розгляну теоретичну частину цього питання, оскільки це відображається на людині, її діях та здібностях досягати результатів.
Потім покажу на реальному прикладі, як «problem solving mindset» допоміг мені вирішити проєктні питання та оптимізувати рішення, виходячи з вимог та поточних обставин.
Щоб соціально-теоретична частина не була «відірваною» від ІТ-контексту, я покажу на власному прикладі, як навичка problem-solving впливає на рутину ІТ-спеціаліста.
Ця стаття має на меті допомогти розробникам усвідомити важливість комплексного підходу до вирішення проблем, що дозволяє вдосконалювати навички та сприяти успішному розвитку проєктів.
Яка твоя персоналія, як розробника?
Впродовж своєї карʼєри мені доводилось багато вчитись, виконувати поставлені задачі, приймати рішення та вирішувати проблеми. Тобто більшість з нас у повсякденному житті вже знайома з такими навичками, як problem-solving та decision making.
У сьогоденні я для себе виділяю такі архетипи:
- The Thinker (Мислитель)
- The Creator (Творець)
- The Doer (Виконавець)
- The Decider (Приймач рішень)
Такі терміни зустрічаються у різних підходах та контекстах.
У літературі з лідерства та продуктивності вони відповідають ключовим ролям або навичкам, які підкреслюються провідними мислителями в сфері організаційної динаміки та самоуправління:
Thinker представляє стратегічний аспект лідерства, зосереджуючись на аналізі ситуацій, виявленні можливостей і формуванні довгострокових цілей. Наприклад, у книзі «The 7 Habits of Highly Effective People» Стівена Кові наголошується на важливості мислення «з кінцевою метою в думках».
Doer — це орієнтована на виконання особистість, яка пріоритетом ставить досягнення результатів. У книзі Девіда Аллена «Як упорядкувати справи» наголошується на важливості розбиття завдань на конкретні дії.
Creator — це новатор, який створює унікальні рішення, працюючи у середовищах, що сприяють експериментуванню та інноваціям, як описано у книзі «Творча впевненість» Тома Келлі та Девіда Келлі.
Decider — приймає обґрунтовані та своєчасні рішення, які базуються на доступних даних. Це центральна роль у принципах лідерства Джона Максвелла, де прийняття рішень є основою ефективного управління.
У методології Design Thinking and Innovation наголошується на людино-центричному підході до вирішення проблем, де ці архетипи природно відображаються на різних етапах процесу проєктування:
Thinker бере участь на етапах «Empathize» та «Define», досліджуючи проблему, збираючи інсайти користувачів і аналізуючи больові точки. Creator зосереджується на етапах «Ideate» та «Prototype», створюючи ідеї та розробляючи прототипи, які можна тестувати й удосконалювати. Doer працює на етапі «Test», реалізуючи рішення, перевіряючи прототипи та забезпечуючи відповідність потребам користувачів. Decider присутній впродовж усього процесу, але особливо важливий у моменти прийняття рішень, наприклад, під час оцінки компромісів, пріоритизації відгуків користувачів або вибору ідей для прототипування.
У сучасному світі ІТ усі персоналії відіграють важливу роль і повинні бути збалансовані, щоб досягти поставлених цілей. На мою думку, ці архетипи можна простежити й у сфері розробки.
Архетип Doer здебільшого переважає на етапах від trainee до middle, оскільки головне — виконати поставлені задачі, реалізувати функціонал і дотриматися дедлайнів. Зі зростанням професійного рівня та збільшенням відповідальності на перший план виходять архетипи Creator та Decider, адже саме їхній майндсет сприяє досягненню успіху та ефективному виконанню завдань.
«Doers become the part of history but creators make the history»
― Myra Yadav
Базуючись на розумінні архетипів та власному досвіді я можу охарактеризувати їх наступним чином:
Doer
Doer — особа, яка бере завдання та виконує їх: участь у практичній розробці, реалізації, тестуванні та деплойменті проєктів.
Сфера відповідальності:
- Написання та підтримка коду (front-end, back-end, full-stack).
- Реалізація проектів та функціоналу згідно визначених рішень.
- Виконання модульного тестування (unit testing), інтеграційного тестування та дебагінгу.
- Деплоймент продукту та забезпечення його безперебійної роботи.
Приклад у розробці:
- Front-end розробник, який створює інтерфейс користувача і забезпечує швидкість реагування на основі варфреймів, наданих дизайнером.
- Back-end розробник, який створює API та інтегрує їх із базами даних та іншими службами. Full-stack, який працює як на клієнті, так і на сервері, щоб реалізувати end-to-end фічі. QA engineer тестує та виявляє помилки у задеплоїному коді до того, як він досягне продакшену.
Creator
Creator фокусується на ідеях, дизайні та стратегії. Наприклад, створення варфреймів та прототипів, визначення технічної архітектури.
Сфера відповідальності:
- Мозковий штурм, пошук інноваційних рішень та ідей щодо продуктів.
- Створення дизайнів, варфреймів і прототипів.
- Визначення технічних архітектур і фреймворків.
- Написання технічних специфікацій, тощо.
Приклад у розробці:
- Designer UX/UI, який створює каркаси та прототипи для нового веб-застосунку.
- Lead architect, який визначає технічний стек і структуру проєкту для складного застосунку.
- Product owner або бізнес-аналітик, який викладає бачення та хайлевельні вимоги до проєкту.
Decider
Decider приймає важливі та критичні рішення щодо продукту та вектору його розвитку, як-от вибір технології, роудмап проєкту, розподіл ресурсів.
Сфера відповідальності:
- Прийняття остаточних рішень щодо технологій, фреймворків або інструментів для використання.
- Пріоритизація функцій і визначення роудмапу продукту.
- Нагляд за графіком виконання проєкту та розподілом ресурсів.
- Управління очікуваннями клієнтів і забезпечення відповідності бізнес-цілей технічним можливостям.
Приклад у розробці:
- Tech lead вирішує, чи використовувати певний фреймворк (React, Vue.js, etc.) для проєкту на основі навичок команди та вимог до проєкту.
- Project manager визначає обсяг проекту, пріоритетність функцій і встановлює етапи.
- CTO, який контролює хайлевельні технологічні рішення, такі як хмарні платформи або архітектури баз даних, щоб забезпечити масштабованість проєкту та придатність до обслуговування й підтримки.
Я свідомо не враховував архетип Thinker у своїй інтерпретації, адже прагну зосередити вашу увагу на попередніх трьох архетипах і їхньому спільному впливі на розвиток кар’єри.
«My observation is that the doers are the major thinkers. The people that really create the things that change this industry are both the thinker and doer in one person»
― Steve Jobs
Перехід від «Виконавця» до «Творця»
Зображення, створене ШІ
Проаналізувавши попередню інформацію, можна прийти до висновків, що з плином часу і зростанням у професії тобі доведеться частіше приймати рішення і вирішувати супутні проблеми технічного та нетехнічного характеру.
Виконавці зазвичай очікують чітко поставлених задач, гарної деталізації проблеми та наставництва. Якщо задача під час виконання потребує додаткового аналізу для прийняття рішення або вирішення ряду проблем, які неможливо було врахувати на етапі проєктування, то така людина може розгубитись або відокремитись від проблеми типу: «Ну це ж не мені вирішувати, хай скажуть, що робити, і я зроблю».
Саме у таких ситуаціях треба починати змінювати свій майндсет, бо можна стати тією людиною, «яка приймає рішення чи яка вирішить проблему». Ці навички потребують часу на опанування. Варто практикувати їх регулярно та тримати баланс між тим, що тобі під силу самому, і тим, коли треба звернутись по допомогу.
На мою думку, існують ключові навички, які значно сприяють опануванню методу «problem-solving»:
- Аналітичні навички (Analytical Skills) допомагають ефективно розбивати проблему на складові частини, аналізувати дані та знаходити основні причини.
- Креативне мислення (Creative Thinking) дозволяє генерувати нестандартні ідеї та підходи для вирішення складних і нетипових завдань.
- Критичне мислення (Critical Thinking) сприяє об’єктивній оцінці ситуації, визначенню сильних та слабких сторін можливих рішень.
- Технічні навички (Technical Skills) — міцна технічна база має вирішальне значення для вирішення проблем. Зосередься на тому, щоб бути в курсі останніх технологій, які стосуються галузі, оскільки нові інструменти та підходи часто можуть надати нові рішення для постійних проблем.
- Навички комунікації (Communication Skills) допомагають пояснити свої ідеї, узгоджувати рішення та ефективно співпрацювати з командою.
Ефективне вирішення проблем вимагає структурованого підходу, щоб забезпечити чіткість і досягнення практичних результатів.
- Визначення проблеми: чітко сформулюйте питання, зрозумійте його суть і масштаби, щоб впевнитись, що ви вирішуєте ключову проблему.
- Збір інформації: зберіть усі необхідні дані, відгуки та інсайти від зацікавлених сторін, щоб отримати всебічне розуміння ситуації.
- Аналіз першопричини: вивчіть основні чинники, які викликають проблему, зосередившись на її глибинних аспектах, а не поверхневих проявах.
- Генерація рішень: розробіть різні варіанти вирішення. Проявляйте креативність і відкритість, пропонуйте різні ідеї і не відкидайте їх на початкових етапах.
- Оцінка й вибір рішення: порівняйте переваги й недоліки кожного варіанту, оцініть ризики та ефективність, обравши найбільш ефективний підхід.
- Впровадження: чіткий план дій, розподіл ресурсів та забезпечення контролю за виконанням для досягнення бажаного результату.
- Перегляд і навчання: оцініть результати, виявіть уроки, чого вдалося навчитися, та вдосконалюйте процес, щоб підвищити ефективність вирішення проблем у майбутньому.
Дотримуючись цих кроків, ви зможете формувати міцний підхід до розв’язання будь-яких викликів структуровано та успішно.
Якщо самостійно не вдається знайти рішення, збери усю інформацію з попередніх кроків і звернись по допомогу. Заздалегідь опиши проблему та надай все, що допоможе людині швидше зрозуміти її контекст. Це може бути відео, текст, діаграма або комбінація цих варіантів. Головна мета — надати розгорнуту інформацію.
Опанування цих практик потребує сміливості брати на себе відповідальність, також важливими є наполегливість та консистентність.
«Consistency is the key to success»
― Unknown
Базуючись на своєму досвіді, я прийшов до таких висновків:
Практика. Чим більше проблем ти вирішуєш, тим кращими стають твої навички. Не треба боятись складних завдань — вони стануть для тебе викликом та допоможуть розвиватися.
Вчись у інших. Співпрацюй з досвідченими професіоналами, долучайся до код-рев’ю та open-source проєктів. Це дасть змогу побачити різні підходи до вирішення задач.
Залишайся зацікавленим. Постійно вивчай нові технології, методології та кращі практики в IT-індустрії. Розуміння сучасних трендів допоможе знаходити інноваційні рішення.
Приймай невдачі. Робити помилки — це нормально. Вони перетворюються у досвід, якщо додати наполегливість і не здаватись. Розцінюй кожну спробу як експеримент, вчись із результатів цих експериментів і тим самим покращуй свої навички вирішення проблем та прийняття рішень.
Шукай зворотний зв’язок. Регулярно звертайся до колег та наставників за оцінкою свого підходу до вирішення задач. Це дозволить краще зрозуміти свої сильні й слабкі сторони та вдосконалювати результати.
Інструменти та методики
Для вдосконалення цих навичок в інтернеті доступна велика кількість інструментів, методик і ресурсів. Нижче наведу декілька з них, якими користуюсь особисто:
Codewars— це платформа, яка допомагає вивчати, тренувати та вдосконалювати свої навички кодування, вирішуючи завдання багатьох типів і рівнів складності. Підтримує велику кількість мов програмування. Реєструйся та вирішуй поставлені задачі. Після вирішення задачі можна подивитись, як її вирішили інші учасники. Це дуже корисно, бо можна подивитись на інші підходи до вирішення однієї проблеми.
скріншот з www.codewars.com
Root cause Analysis — це ключовий етап у процесі вирішення проблем, який дозволяє виявити не тільки симптоми, а й основні фактори, що їх спричиняють. Без цього кроку є ризик вирішувати лише поверхневі прояви проблеми, залишаючи справжню причину невиявленою. Для ефективного аналізу важливо ставити правильні запитання: «Чому це сталося?», «Які процеси, дії чи умови призвели до цієї ситуації?» і використовувати техніки, як-от метод «5 Чому». Фактично це саме ті процеси, які я описував раніше.
5 Why Analysis — це проста і потужна техніка, яка дозволяє дістатися до основної причини проблеми шляхом послідовного запитання «Чому?». Метод починається з визначення конкретної проблеми, а кожна відповідь на запитання «Чому це сталося?» стає основою для наступного питання. Зазвичай п’яти кроків достатньо, щоб виявити глибинну причину, хоча кількість «Чому» може змінюватися залежно від складності ситуації. Наприклад, якщо система працює нестабільно, ми запитуємо: «Чому виникла нестабільність?», «Чому система не була протестована?» і так далі. Цей підхід ефективний, оскільки зосереджує увагу на першопричинах, а не на симптомах, і дозволяє уникнути поверхневих рішень.
Kaizen, що перекладається як «безперервне вдосконалення», — це японська філософія управління, яка успішно використовується в IT-індустрії для оптимізації процесів, підвищення продуктивності та покращення якості продуктів. Її основна ідея — впровадження невеликих, поступових змін, які з часом накопичуються та призводять до значних покращень. У контексті IT це може включати регулярне код-рев’ю, автоматизацію рутинних завдань, впровадження DevOps-практик чи вдосконалення Agile-процесів.
Kaizen також сприяє формуванню культури відкритості, де кожен член команди може пропонувати ідеї для покращення. Наприклад, під час розробки програмного забезпечення застосування Kaizen допомагає не тільки виправляти помилки, а й аналізувати їхні причини, щоб уникнути повторень. Цей підхід стимулює інновації, покращує командну роботу та забезпечує сталість у досягненні довгострокових цілей.
Зазначені методики я використовував для вирішення проблеми продуктивності у застосунку, який ми розроблюємо, і далі я покажу, як саме.
Навички вирішення проблем є фундаментом для переходу до ролей Творця (Creator) та Приймача Рішень (Decider). Ось як вони цьому сприяють:
- Інноваційне мислення. Ці навички стимулюють креативність, дозволяючи створювати інноваційні рішення, продукти чи системи замість простого виконання задач.
- Стратегічне бачення. Вони допомагають мислити глобально, формувати та структурувати ідеї, що відповідають довгостроковим цілям.
- Проактивне ставлення. Завдяки вирішенню проблем твій підхід змінюється з очікування інструкцій на ініціативність, що дає змогу пропонувати оригінальні ідеї та покращення.
- Обґрунтоване прийняття рішень. Навички вирішення проблем покращують аналітичні здібності, допомагаючи оцінювати ризики, варіанти й ухвалювати продумані рішення, які сприяють успіху проєкту.
- Лідерство та відповідальність. Вони готують до взяття відповідальності за складні виклики, роботи в умовах невизначеності та надання чітких вказівок команді.
- Вирішення конфліктів. Приймачі рішень часто мають справу із суперечливими пріоритетами, а сильні навички вирішення проблем допомагають їх узгоджувати та правильно розставляти пріоритети.
Розвиваючи ці навички, ти переходиш від виконання завдань до формування стратегій (Creator) та ухвалення рішень із впливом (Decider), тим самим прискорюючи свій кар’єрний і лідерський розвиток.
Моя «проблема», та як я її вирішував
До мене звернулися за допомогою у вирішенні питання продуктивності (performance) продукту, який розробляє компанія, де я працюю, — Valtech. Якщо коротко, це чат-бот, який використовує Next.js як вебфреймворк та принципи MACH (Microservice, API-first, Cloud-Native, Headless) для реалізації цього завдання. Це все, що мені було відомо до моменту долучення до проєкту.
У цьому тексті я покажу, як вдалося покращити продуктивність вебінтерфейсу та позбутися бойлерплейт-коду, що позитивно вплинуло на досвід розробника (Developer Experience).
Зображення 1. Інтерфейс чатботу
Аналіз ситуації та визначення цілей
Для початку важливо конкретно визначити, яку проблему ми намагаємось вирішити. Питання продуктивності може бути багатогранним і стосуватися оптимізації фінального коду, лейзі-лоудингу, код-сплітингу, налаштувань сервера, кешу, CDN тощо. Воно також може включати показники core web vitals або конкретні проблеми, з якими стикається користувач чат-боту.
Дотримуючись методики Root Cause Analysis, я зосередився на виявленні реальної причини, а не лише на поверхневих симптомах. Команда розробників і QA-інженерів виділила найпомітніші та найчастіші проблеми, що впливали на користувачів. Основна скарга полягала у зависанні інтерфейсу під час введення тексту в чаті. Це викликало затримки в обробці запитів, особливо коли у користувачів була велика історія чатів.
Можливо, вам знайомий цей досвід із розробки: коли друкуєш текст, а він з’являється з помітною затримкою, і при цьому весь застосунок «заморожений» та не реагує на дії користувача. Чим більше створених чатів і чим довша їхня історія, тим серйознішими ставали ці «глюки».
Зображення 2. «Глюк» під час набору тексту з довгою історією чатів і великою кількістю створених чатів (тривалість GIF 15 с.)
Раніше я мав досвід вирішення подібних проблем, але тоді це стосувалось сайтів із численними табами, наповненими складними формами. У випадку чат-боту ситуація простіша: тут є лише одне поле — Textarea для введення promptʼів.
Я поставив собі ключові запитання:
- Чому інтерфейс зависає?
- Чи є зайві операції під час відображення або обробки даних?
- Які ресурси перевантажуються у цих процесах?
Аналіз логів та використання React Profiler показали, що надмірні повторні ререндери DOM-елементів є основною причиною проблеми. Це стало відправною точкою для подальших кроків.
Підбір інструментів для дебагінга
Зазвичай для ефективного відстежування та вирішення проблем, пов’язаних із React, я рекомендую використовувати інструменти розробника для React (React DevTools).
У моєму випадку React Profiler став дуже корисним. До речі, не всі розробники мають звичку його використовувати, тим самим втрачаючи можливість спробувати цей чудовий інструмент для вирішення задач. Якщо ти ще не використовував Profiler, раджу спробувати.
React Profiler — це потужний інструмент у React DevTools, що допомагає аналізувати й оптимізувати продуктивність застосунків React. Він надає детальну візуалізацію того, як відображаються компоненти, скільки часу займає кожен рендер і які взаємодії його запускають. Це дозволяє виявляти вузькі місця (bottlenecks) продуктивності та точно налаштовувати поведінку компонентів.
З декларативним підходом React до рендерингу іноді важко відстежувати непотрібні рендери або покращувати продуктивність. React Profiler вирішує цю проблему, надаючи можливість переглядати життєвий цикл компонента в реальному часі та бачити, як працює React, і де можна внести покращення.
Використовуючи Profiler, розробники можуть візуалізувати рендери й аналізувати, чому певні компоненти оновлювались, отримуючи дані для прийняття обґрунтованих рішень.
Крім того, існує безліч інших інструментів для тестування та дебагінгу у браузері, таких як Performance Tab, Performance Insight Tab (скоро стане застарілим, а його функціонал перенесуть до Performance Tab), Lighthouse Tab тощо.
Зображення 3. Chrome DevTools із встановленим React DevTools
Пошук проблемних місць коду
Відкривши та запустивши Profiler, я отримав перші результати — це стало моєю вхідною точкою для виявлення проблеми. Надалі ці дані будуть використані для оцінки успішності рефакторингу та визначення, чи вдалося покращити ситуацію.
Зображення 4. Чат-бот з Profiler
Зображення 5. Результати Profiler
Зображення 6. Результати Profiler
Наступним кроком у пошуку проблеми було зрозуміти, як саме дані потрапляють у компоненти, де вони зберігаються, та як ними маніпулюють. Я детально заглибився в архітектуру застосунку та розглянув потік даних (data flow). Після перегляду репозиторію я зрозумів, як влаштована система як і виглядає загальна архітектура.
Розробка чат-боту починалась як MVP (minimum viable product) і мала слугувати blueprint для майбутніх проєктів. У процесі розробки blueprint-команда активно експериментувала, створювала різні PoC (Proof of Concept) і розширювала продукт. На початку функціонал був мінімальним, тож архітектура виглядала так:
- Next.js — вебфреймворк на React із великою кількістю вбудованого функціоналу;
- Fetch API — отримання даних;
- React Context — стейт-менеджмент застосунку;
- Material UI — бібліотека візуальних компонентів;
- інші допоміжні бібліотеки.
Далі я використав методику 5 Whys Analysis для детального розуміння причин:
- Чому виникають ререндери? — Дані в React Context змінюються занадто часто.
- Чому так багато змін у Context? — Весь глобальний стейт зберігається в одному контексті, що створює зайві залежності між компонентами.
- Чому обрали такий підхід? — Спочатку це був MVP, де структура стейту не оптимізовувалася для продуктивності.
- Чому не було зроблено оптимізацій? — На етапі PoC (Proof of Concept) продуктивність не була пріоритетом.
- Що можна зробити зараз? — Перейти до модульного підходу у стейт-менеджменті, використовуючи Zustand.
Ознайомившись із проєктом та його проблемою, я зрозумів, що першим кроком має бути оптимізація управління даними. React Context можна використовувати для роботи з глобальним станом застосунку, але лише за відповідних умов. Застосування React Context для управління всім стейтом сайту може призводити до численних зайвих оновлень інтерфейсу, що погіршує продуктивність. Чим більша залежність додатку від даних і стану, тим частіше виникають такі зайві оновлення.
У деяких випадках React Context у поєднанні з useReducer може бути корисним, але для чат-боту з великим функціоналом це створює додаткові труднощі. Оскільки багато компонентів повинні взаємодіяти між собою, важливо уникнути передачі даних через численні рівні компонентів (props drilling).
На цьому етапі треба вирішити, як рухатися далі та які зміни впровадити для розв’язання проблеми. Це й стало моментом прийняття рішень. У мене з’явилося кілька варіантів, і я перерахую їх, щоб продемонструвати хід думок у пошуку рішення:
- React Context + useReducer.
- Redux Toolkit.
- Zustand.
- Інші стейт-менеджери та бібліотеки зі схожим функціоналом.
Робота над покращенням продуктивності
Ми з командою вирішили замінити React Context і перенести весь стейт у Zustand. Цю бібліотеку обрали через її компактний розмір і легкий поріг входження порівняно з Redux Toolkit. Хоча Redux Toolkit позбавився значної частини boilerplate-коду, він усе ще залишається громіздким.
Zustand — це мінімалістична та ефективна бібліотека для управління стейтом, яка допомагає уникнути зайвого boilerplate-коду та повторних ререндерів.
Основні переваги для нас:
- Selective Subscriptions — завдяки селекторам компоненти повторно рендеряться лише тоді, коли змінюється та частина стану, яку вони використовують.
- Minimal Boilerplate — стор застосунку можна налаштувати за допомогою лише кількох рядків коду через метод create.
- Ease of use — немає потреби у провайдерах або ред’юсерах, а прямий доступ до стейту значно спрощує робочі процеси.
- Bundle Size — Zustand дуже легкий (~2 КБ), що робить його ідеальним вибором для програм із фокусом на продуктивність.
Для прийняття рішення ми створили порівняльну таблицю:
Характеристика |
Zustand |
React Context |
Redux Toolkit |
Продуктивність |
Точкові оновлення з селекторами |
Перерендер усіх споживачів |
Ефективна, але залежить від реалізації |
Простота використання |
Мінімальна конфігурація, інтуїтивний API |
Простий для статичних даних |
Покращений у RTK, але все ще громіздкий |
Масштабованість |
Легко справляється з великими застосунками |
Обмежений, призводить до «контекстного пекла» |
Ідеальний для великих застосунків |
Крива навчання |
Низька |
Низька |
Середня до високої |
Підтримка Middleware |
Вбудована |
Потрібна кастомна реалізація |
Підтримка з коробки |
Збереження даних |
Вбудоване |
Ручна реалізація |
Потрібне використання middleware |
Розмір пакету |
~2 KB Примітка: надзвичайно легкий і оптимізований для продуктивності. Підходить для сучасних модульних застосунків. |
0 KB Примітка: вбудований у React, тому немає додаткових витрат на розмір пакету. |
~22 KB Примітка: містить багато вбудованих функцій (наприклад, ред’юсери, middleware, DevTools), що збільшує розмір. |
Попередній глобальний контекст був замінений модульним підходом з використанням Zustand, а окремі частини стейту були винесені у окремі файли для кращої читабельності та підтримки.
import { create } from "zustand"; import { messagesState, IMessageCollectionState } from "./messages"; import { IReferencesCollectionState, referencesState } from "./references"; import { ISettingsCollectionState, settingsState } from "./settings"; import { ITopicsCollectionState, topicsState } from "./topics"; interface IZustandStore extends IMessageCollectionState, IReferencesCollectionState, ISettingsCollectionState, ITopicsCollectionState { messageEnteredByUser: string; setMessageEnteredByUser: (messageEnteredByUser: string) => void; sourceTab: number; setSourceTab: (sourceTab: number) => void; } export const useStore = create<IZustandStore>((set) => ({ ...messagesState(set), ...referencesState(set), ...settingsState(set), ...topicsState(set), messageEnteredByUser: "", setMessageEnteredByUser: (messageEnteredByUser) => set({ messageEnteredByUser, }), sourceTab: 0, setSourceTab: (sourceTab) => set({ sourceTab, }), }));
import { Message, MessageCollection } from '@types/Message.types'; import { initialMsgCollection } from '../../hooks/stateHooks/useMessageCollection'; import { createNewMessageCollection } from './messages.utils'; import { SetFunction } from '..'; import { DEFAULT_TOPIC_ID } from '@const/defaults'; export interface IMessageCollectionState { messages: { data: MessageCollection[]; loading: boolean; error: Error | null; }; setMessages: (messages: MessageCollection[]) => void; setMessagesLoading: (loading: boolean) => void; setMessagesError: (error: Error | null) => void; setNewMessage: (message: Message, topicId: string) => void; addTopicToMessages: (history: Message[], topicId: string) => void; clearDefaultMessageCollection: () => void; } export const messagesState = ( set: SetFunction<IMessageCollectionState>, ): IMessageCollectionState => { return { messages: { data: [initialMsgCollection], loading: false, error: null, }, setMessages: data => set(state => ({ messages: { ...state.messages, data, }, })), setMessagesLoading: loading => set(state => ({ messages: { ...state.messages, loading, }, })), setMessagesError: error => set(state => ({ messages: { ...state.messages, error, }, })), setNewMessage: (message, topicId) => set(state => ({ messages: { ...state.messages, data: createNewMessageCollection(state.messages.data, message, topicId), }, })), addTopicToMessages: (history, topicId) => set(state => ({ messages: { ...state.messages, data: [...state.messages.data, { history, topicId }], }, })), clearDefaultMessageCollection: () => set(state => ({ messages: { ...state.messages, data: state.messages.data.map((messageState) => messageState.topicId === DEFAULT_TOPIC_ID ? initialMsgCollection : messageState ), }, })), }; };
Ми застосували таку ж оптимізацію і до інших частин стейту. Однак це не єдині покращення, впроваджені під час рефакторингу. Наступним кроком стало розбиття великих React-компонентів на менші. Більшість бізнес-логіки була винесена у кастомні хуки, а всю розмітку, яку можна було виділити в окремі компоненти, перемістили в окрему папку із сабкомпонентами. Ось приклад одного з компонентів після рефакторингу:
Зображення 7. Перед застосуванням принципів «чистого коду»
Зображення 8. Після застосування принципів «чистого коду»
Для оптимізації продуктивності React-компонентів я також додав useMemo
та useCallback
усюди, де це було доречно. Важливо розуміти, коли ці інструменти дійсно покращують продуктивність, а коли їх використання стає зайвим overengineering, що, навпаки, може нашкодити (в інтернеті можна знайти багато інформації на цю тему).
Окремо хочу показати приклад використання React.memo для оптимізації компонентів із кастомною перевіркою, коли необхідно самостійно писати логіку порівняння prevProps і nextProps:
//React.memo with custom props comparison export default memo(MessageList, (prevProps, nextProps) => { const prevChatHistory = prevProps.currentChatHistory; const prevChatHistoryLength = prevChatHistory.length; const nextChatHistory = nextProps.currentChatHistory; const nextChatHistoryLength = nextChatHistory.length; // Only re-render if the loading state changes or the chat history changes return ( prevProps.isLoading === nextProps.isLoading && prevProps.hasErrors === nextProps.hasErrors && prevProps.currentChatHistory.length === nextProps.currentChatHistory.length && prevChatHistory[prevChatHistoryLength - 1]?.id === nextChatHistory[nextChatHistoryLength - 1]?.id ); });
Після переходу від React Context до Zustand розбиття складних компонентів на простіші та застосування оптимізацій, які надає React (memo, useMemo, useCallback
), я перевірив продуктивність ще раз за допомогою React Profiler.
Результат Profiler з Zustand:



Як видно зі скріншотів, результати значно покращились. Я провів декілька тестів у різних умовах та з різним стейтом. Тепер набір тексту більше не викликає зависання всього застосунку, і користувачі не відчуватимуть роздратування через постійні «глюки» системи.
Все, чи можна зробити краще?
Зображення, створене ШІ
Покращувати та оптимізувати код можна до нескінченності. Важливо знати межу, оцінювати, скільки часу є в запасі, і чи є це пріоритетом для проєкту.
Водночас писати чистий код і прагнути зробити речі кращими — це надзвичайно цінна риса для розробника.
Одне з правил бойскаутів говорить: «Залишай табір чистішим, ніж ти його знайшов». У програмуванні це правило трактується як підхід до роботи з кодом: «Залишай код у кращому стані, ніж той, у якому ти його знайшов.» Цей принцип особливо важливий для підтримання високої якості коду в довготривалих проєктах.
«Always leave the code you’re editing a little better than you found it»
— Robert C. Martin (Uncle Bob)
Ось як це виглядає на практиці:
- Якщо працюєш із функцією чи модулем, залиш його більш читабельним, зрозумілим або структурованим. Наприклад, перейменуй змінну на більш зрозумілу, видали непотрібний коментар чи оптимізуй логіку.
- Побачивши неефективний або застарілий код, не залишай його таким, якщо можна швидко покращити. Це може включати видалення дубльованого коду або поділ великої функції на менші частини.
- Змінюючи одну частину коду, враховуй, як це вплине на суміжні частини, і виправ їх, якщо потрібно.
- Завжди залишай код у такому стані, щоб наступний розробник швидше його зрозумів.
Дотримання правила бойскаутів підвищує підтримуваність коду, зменшує технічний борг і прискорює розробку завдяки зрозумілому та читабельному коду.
«Any fool can write code that a computer can understand. Good software developers write code that humans can understand»
Martin Fowler
Оскільки основну проблему з продуктивністю вдалося вирішити, постало інше питання: «Що можна зробити, щоб написання цього функціоналу було простішим для розробника?»
Для нашого проєкту відповіддю на це стало використання бібліотеки Tanstack Query (раніше React Query). Вона фокусується на серверному стейті, а не на клієнтському. Чому саме Tanstack Query? Тому що наш застосунок здебільшого залежить від серверних даних, і клієнтський стейт не потребує зберігання всіх чатів і повідомлень. Клієнтський стейт має лише керувати діями користувача, зберігати певні налаштування та уникати «props drilling».
Ось які покращення ми отримали після інтеграції Tanstack Query:
- Зменшення навантаження на клієнтський стейт, оскільки більша частина даних тепер зберігається у кеші бібліотеки.
- Зниження обсягу boilerplate-коду та ручних операцій завдяки готовим механізмам бібліотеки (out-of-the-box data fetching lifecycles & cache).
- Zustand добре підходить для локального стану, але не має спеціалізованих інструментів для асинхронного отримання, кешування чи повторного отримання даних, які надає Tanstack Query.
Інтеграція Tanstack Query знижує складність керування життєвим циклом даних, зменшує шаблонність коду, покращує продуктивність і підвищує досвід розробника (Developer Experience). У результаті ми отримуємо чистіший код і задоволених розробників.
Ось як значно спростився message state порівняно з Zustand — messages.ts, яку я показував раніше.
//Messages state with Zustand & Tanstack Query - messages.ts import { SetFunction } from '..'; const CHAT_HISTORY_KEY = 'chatHistory'; export interface IMessageCollectionState { queryKeys: { messages: (topicId: string, count: number) => [string, string, number]; }; regeneratingMessageId: string | null; setRegeneratingId: (regeneratingMessageId: string | null) => void; messageCount: number; setMessageCount: (messageCount: number) => void; } export const messagesState = ( set: SetFunction<IMessageCollectionState>, ): IMessageCollectionState => { return { regeneratingMessageId: null, setRegeneratingId: (regeneratingMessageId) => set({ regeneratingMessageId }), messageCount: 0, setMessageCount: (messageCount) => set({ messageCount }), queryKeys: { messages: (topicId: string, count: number) => [CHAT_HISTORY_KEY, topicId, count], }, }; };
Для отримання історії чату було створено окремий хук useChatHistory
, який об’єднав у собі Zustand (отримання ID вибраного користувачем чату), динамічне керування ключами для Tanstack (функція messagesQueryKey(currentTopicId, count)
) та інші мінорні речі. Tanstack Query було використано для керування життєвим циклом отримання даних і їхнього кешування для подальшої передачі в UI-компоненти.
//useChatHistory with Zustand & Tanstack Query - useChatHistory.ts type UseChatHistoryReturn = { getChatHistory: (count: number) => void; messages: Message[]; isLoading: boolean; isFetching: boolean; error: Error | null; shouldShowLoadPrevious: boolean; }; export const useChatHistory = (): UseChatHistoryReturn => { const [triggerQuery, setTriggerQuery] = useState<boolean>(false); const currentTopicId = useStore((state) => state.topics.currentTopicId); const { count, setCount, messagesQueryKey } = useStore( useShallow((state) => ({ count: state.messageCount, setCount: state.setMessageCount, messagesQueryKey: state.queryKeys.messages, })), ); const { data: messages, isLoading, error, isFetching, } = useQuery({ queryKey: messagesQueryKey(currentTopicId, count), queryFn: () => fetchHistory({ topicId: currentTopicId, count }), staleTime: Infinity, enabled: triggerQuery, }); const getChatHistory = (newCount = 0): void => { setCount(newCount); setTriggerQuery(true); }; return { messages, isLoading, error, getChatHistory, isFetching, shouldShowLoadPrevious: count > 0, }; };
Після впровадження нової бібліотеки я повторно протестував застосунок на продуктивність і регресію у функціоналі. Переконавшись, що все працює коректно, команда розпочала переписування інших частин коду під Tanstack Query.
Це дозволило покращити досвід розробника, зменшити обсяг коду, який потрібно писати під час додавання нових API-ендпоінтів, і зробити код чистішим та зрозумілішим.
Я вирішив реалізовувати зміни поступово, відповідно до філософії Kaizen — безперервного вдосконалення. Першим кроком стала заміна React Context на Zustand, що дозволило:
- Зменшити зайві ререндери завдяки селективним підпискам.
- Спростити код і уникнути громіздких провайдерів.
Наступним етапом було розбиття великих React-компонентів на менші. Бізнес-логіку було винесено у кастомні хуки, а складні компоненти поділено на сабкомпоненти.
Для довгострокової стійкості та запобігання аналогічним проблемам ми інтегрували Tanstack Query, яка спростила управління життєвим циклом запитів до сервера, забезпечила автоматичне кешування та зменшила обсяг шаблонного коду.
Застосування Root Cause Analysis допомогло ідентифікувати справжню причину проблеми. Метод 5 Whys дозволив деталізувати етапи прийняття рішень, а підхід Kaizen забезпечив поступовий і ефективний перехід до оптимізованої архітектури. У результаті вдалося:
- Зменшити затримки під час введення тексту.
- Спростити підтримку коду завдяки меншій складності.
- Покращити користувацький досвід (UX) і досвід розробника (DX).
Такий підхід до вирішення проблем не лише впорався з поточними задачами, а й заклав надійну основу для подальшого розвитку продукту.
Підсумки
У цій статті я хотів підкреслити важливість навичок «вирішення проблем» у ІТ для розробників. Моя мета — показати, як перехід від ролі «Виконавця/Doer» до «Творця/Creator» і «Приймача Рішень/Decider» сприяє професійному зростанню, а також наголосити на важливості мислення, що зосереджується не лише на виконанні завдань, але й на проактивному вирішенні проблем і прийнятті рішень.
Це значно сприяє кар’єрному зростанню. Розробники зазвичай починають із ролі «Виконавця», але з часом, розвиваючись професійно, переходять до ролей, які передбачають створення нових рішень щодо технологій та напрямку проєкту.
Я поділився власним досвідом і продемонстрував, як застосування навичок «вирішення проблем» допомогло мені покращити продуктивність чат-боту, зокрема вирішити проблему надмірних ререндерів. Я також наголосив на важливості підтримання чистого, зрозумілого та ефективного коду відповідно до «правила бойскаутів» — залишати код у кращому стані, ніж той, у якому ти його знайшов.
На мою думку, перехід від «Виконавця» до «Творця» і «Приймача Рішень» значно прискорює кар’єрний розвиток, адже розвиваються лідерські якості, здатність приймати рішення та навички вирішення проблем.
Я також показав, як аналіз і оптимізація коду можуть суттєво покращити продуктивність застосунку. Писати чистий, читабельний і ефективний код означає не тільки підвищення продуктивності, а й підтримання довгострокової стійкості проєкту та ефективну співпрацю між розробниками.
7 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів