javascript

Оптимизация кода. Что быстрее: циклы vs стрелочные функции. Простая задача с собеседования

  • понедельник, 19 января 2026 г. в 00:00:04
https://habr.com/ru/articles/986224/

Привет, Хабр!

Многие разработчики пишут код в условиях неопределенности. Перечислим их:

  • Недостаток требований

  • "В первый раз". Новая задача через исследование

  • Торопятся

  • Знают только некоторые приемы, их используют везде

  • Не погружаются глубже в задачу

  • Применяют допущения, которые работают не во всех случаях

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

Введение

Это не конкретно поставленная задача на решение, а упражнение "на подумать".

Предыстория:

Обычная жизненная ситуация меня натолкнула на написание этой статьи.

Сцена:

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

Парень называет позицию: ящик 24 банки энергетика. Водитель матерится: "Блин, где же этот ср....й ящик". Крутится внутри машины, глазами выискивает и не может найти.

В магазине, как правило, работает 2 сотрудника - один уже принимает товар на улице, а второй внутри - на кассе. Возле прилавка скопилась очередь.

Мне сразу пришла в голову IT-аналогия - менеджер в роли "приемщика", программисты в ролях продавца и водителя. И я, как юзер, смотрю на это и жду очередь, не могу быстро купить необходимое.

Архитектура есть, процессы есть, а работает медленно.

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

Любой код является декомпозированным списком последовательных инструкций. Разница только в том - насколько крупные или мелкие "кирпичики" разбиения.

Можно мельчить - каждая строка как пункт списка "сделай то", можно в функции/методы оборачивать, а можно и в модули или сервисы. Чтобы было проще видеть картину в целом или разбивать на частности.

Как говорит практика, в одном таком "блоке" чаще всего и возникает "затык", который может являться низкопроизводительным, неоптимальным, и влиять на поведение системы.

Данная статья посвящена поиску таких проблемных мест.

Предварительные соображения

Существует несколько формулировок задачи. Одна из них примерно такая:

Привести пример, когда циклы быстрее стрелочных функций

Как я уже говорил, это не конкретная задача, а "на подумать". Разбор данного примера, надеюсь, даст понимание читателям, как искать в своем коде точки роста производительности.

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

Это конечно не ответ на вопрос, всё-таки хочется получить какое-то более четкое понимание и знать куда смотреть.

Что вообще такое стрелочная функция или лямбда? Если не вдаваться в детали, это синтаксическая обертка, которая позволяет писать более структурированный код. Но у таких конструкций есть правила использования и собственное поведение, которое было задумано для конкретных вещей.

По факту у нас есть некоторая инкапсуляция - есть функционал, у него присутствуют:

  • Синтаксис

  • Что-то делает, выдает какой-то результат

  • Как-то работает под капотом

Ну а циклы - это более простая конструкция, конкретный код. Циклы типа foreach для коллекций уже обертки.

И, главное, в большинстве привычных операций (хотя есть отличия в разных языках), "стрелочки" работают с копиями.

Теперь можно вернуться к конкретной задаче - что быстрее и привести пример.

Задача

Описать конкретный пример, когда циклы работают быстрее "стрелок".

Рассуждаем так: циклы кажутся предпочтительнее, когда количество элементов операций достаточно большое. Потому что, при работе с лямбдами, такими, как map, forEach у нас будут происходить вызовы дополнительных внутренних функций + копирование данных в новые коллекции, что не всегда необходимо.

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

Сделаем на JavaScript (можно на TS, без разницы). Возьмем 100к элементов.

let listObjectsOne = [];
let listObjectsTwo = [];

//Набьем циклом списки
for (i = 0; i < 100000; i++) {


	listObjectsOne.push({
						id: i + 1,
						name: "Name " + i,
						comment: "Comment " + i
					});

	listObjectsTwo.push({
						id: i + 2,
						name: "Name " + i,
						comment: "Comment " + i
					});

} 

Здесь мы просто создаем 2 практически одинаковых списка.

Просто пройдемся и прибавим +1 и +2 к id-шникам.

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

И пример становится на свое место. Возьмем такую формулировку:

Даны два списка элементов listObjectsOne и listObjectsTwo одинакового размера 100к+ элементов. Нужно преобразовать все id-шники элементов следующим образом: все поля id объектов первого списка умножить на 2, все поля id объектов первого списка умножить на 7. И отдать списки далее для использования.

Простейшее решение - map

console.log("Выведем время для Первого случая - map");

let now = new Date().getTime();


listObjectsOne.map( x => {
	
    x.id = x.id * 2
    
    return x;

});
	
listObjectsTwo.map( x => {

    x.id = x.id * 7
    
    return x;

});

console.log (new Date().getTime() - now);

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

19 ms. Неплохо.

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

Т.е. мы проходим с помощью map 2 раза. 2*100к проходов.

А что, если сделать в 2 раза меньше?

И вот здесь мы придумываем циклы. Добавим простейший код ниже для сравнения:


console.log("Выведем время для второго случая - for")

now = new Date().getTime();


for (i = 0; i < listObjectsOne.length; i++) {

	listObjectsOne[i].id = listObjectsOne[i].id * 2;
	listObjectsTwo[i].id = listObjectsTwo[i].id * 7;

}


console.log (new Date().getTime() - now);

Получаем:

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

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

Привожу весь код в развороте.

Скрытый текст

let listObjectsOne = [];
let listObjectsTwo = [];

//Набьем циклом списки
for (i = 0; i < 100000; i++) {


	listObjectsOne.push({
						id: i + 1,
						name: "Name " + i,
						comment: "Comment " + i
					});

	listObjectsTwo.push({
						id: i + 1,
						name: "Name " + i,
						comment: "Comment " + i
					});

} 


console.log("Выведем время для Первого случая - map");

let now = new Date().getTime();


listObjectsOne.map( x => {
	
		x.id = x.id * 2
		
		return x;
	
});
	
listObjectsTwo.map( x => {

    x.id = x.id * 7
    
    return x;

});

console.log (new Date().getTime() - now);





console.log("Выведем время для второго случая - for")

now = new Date().getTime();


for (i = 0; i < listObjectsOne.length; i++) {

	listObjectsOne[i].id = listObjectsOne[i].id * 2;
	listObjectsTwo[i].id = listObjectsTwo[i].id * 7;

}


console.log (new Date().getTime() - now);

А другие языки?

А там тоже самое. Быстро набросал такую же ситуацию на Kotlin.

Особый интерес вызвал кейс вызова Java Steam метода parallelStream на сравнительно большом списке в 10M элементов.

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

В итоге самый быстрый:

val nowMiliSecondsParallelStream = Instant.now().toEpochMilli();


logger.info("Первый")

listOne.parallelStream().forEach {
    (it.id * 2)
}

logger.info(" Второй")

listTwo.parallelStream().forEach {
    (it.id * 2)
}

val intervalMiliSecondsParallelStream = (Instant.now().toEpochMilli() - nowMiliSecondsParallelStream)

logger.info("$intervalMiliSecondsParallelStream милисекунд на создание")

Прилагаю весь код в развороте.

Примеры на Java/Kotlin
fun experimentalMethod() : String {

    val listOne : MutableList<TestingClassOne> = mutableListOf()

    val listTwo: MutableList<TestingClassTwo> = mutableListOf()

    for (i  in 1..10_000_000) {

        listOne.add(
            TestingClassOne(
                id = i,
                name = "Name $i",
                comment = "Comment $i"
            )
        )

        listTwo.add(
            TestingClassTwo(
                id = i * 2,
                name = "Name $i",
                comment = "Comment $i",
                data = "$i $i $i"
            )
        )

    }

    logger.info("Тестирование forEach из Котлин")
    logger.info(" ")

    val nowMiliSeconds = Instant.now().toEpochMilli();

    logger.info(" Первый")

    listOne.forEach {
        (it.id * 2)
    }

    logger.info(" ВТорой")

    listTwo.forEach {
        (it.id * 2)
    }

    val intervalMiliSeconds = (Instant.now().toEpochMilli() - nowMiliSeconds)

    logger.info("$intervalMiliSeconds милисекунд на создание")

    logger.info(" ")

    logger.info("Тестирование forEach с asSequence из Котлин")
    logger.info(" ")


    val nowMiliSecondsSequense = Instant.now().toEpochMilli();

    logger.info(" Первый")

    listOne.asSequence().forEach {
        (it.id * 2)
    }

    logger.info(" ВТорой")


    listTwo.asSequence().forEach {
        (it.id * 2)
    }

    val intervalMiliSecondsSequence = (Instant.now().toEpochMilli() - nowMiliSecondsSequense)


    logger.info("$intervalMiliSecondsSequence милисекунд на создание")

    logger.info(" ")


    logger.info("Тестирование стрима")
    logger.info(" ")

    val nowMiliSecondsStream = Instant.now().toEpochMilli();


    logger.info(" Первый")

    listOne.stream().forEach {
        (it.id * 2)
    }

    logger.info(" Второй")

    listTwo.stream().forEach {
        (it.id * 2)
    }

    val intervalMiliSecondsStream = (Instant.now().toEpochMilli() - nowMiliSecondsStream)

    logger.info("$intervalMiliSecondsStream милисекунд на создание")



    logger.info(" ")

    logger.info("Тестирование forEachIndexed")


    val nowMiliSecondsFor = Instant.now().toEpochMilli();


    logger.info(" Первый")

    listOne.forEachIndexed{index, element ->
        element.id * 2

        listTwo[index].id * 2


    }

    val intervalMiliSecondsForResult = (Instant.now().toEpochMilli() - nowMiliSecondsFor)

    logger.info("$intervalMiliSecondsForResult милисекунд на создание")

    logger.info(" ")


    logger.info("Тестирование for old school")


    val nowMiliSecondsForOldSchool = Instant.now().toEpochMilli();


    logger.info(" Первый")


    for (i in 0..<listOne.size) {
        listOne[i].id * 2
        listTwo[i].id * 2
    }


    val intervalMiliSecondsForResultOldSchool = (Instant.now().toEpochMilli() - nowMiliSecondsForOldSchool)

    logger.info("$intervalMiliSecondsForResultOldSchool милисекунд на создание")


    logger.info(" ")

    logger.info("Тестирование parallel стрима")
    logger.info(" ")

    val nowMiliSecondsParallelStream = Instant.now().toEpochMilli();


    logger.info("Первый")

    listOne.parallelStream().forEach {
        (it.id * 2)
    }

    logger.info(" Второй")

    listTwo.parallelStream().forEach {
        (it.id * 2)
    }

    val intervalMiliSecondsParallelStream = (Instant.now().toEpochMilli() - nowMiliSecondsParallelStream)

    logger.info("$intervalMiliSecondsParallelStream милисекунд на создание")

    logger.info(" ")

    return "Результат вычислений"

    }

И вывод лога

Тестирование forEach из Котлин
   
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 493 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Тестирование forEach с asSequence из Котлин
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 496 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Тестирование стрима
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 430 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Тестирование forEachIndexed
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 383 милисекунд на создание
NFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Тестирование for old school
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 342 милисекунд на создание
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Тестирование parallel стрима
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Первый
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Второй
INFO 4316 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 131 милисекунд на создание

Обратим внимание, что на Kotlin были другие операции, умножение на 2 обоих id-шников, а не на 7. Здесь смысл в том, что внутри итераций могут быть абсолютно любые преобразования, функции, еще что-то. Это те самые O(n), о которых принято говорить.

2 цикла в виде foreach или map в примерах на JS - это O(2n), а в общем старом цикле O(n). В параллельных даже не буду указывать, потому что быстрее, это хорошо, но дорого по потокам.

А если через БД?

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

В микросервисной архитектуре популярен REST. Возьмем его.

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

В рамках данного разбора что мы имеем - списки примерно такого содержания на выходе:

[{id: 4, name: 'Name 0', comment: 'Comment 0'},
{id: 8, name: 'Name 1', comment: 'Comment 1'},
....
]

и

[
{id: 49, name: 'Name 0', comment: 'Comment 0'},
{id: 98, name: 'Name 1', comment: 'Comment 1'},
....
]

Прямой запрос к БД именно для конкретной задачи никаких преимуществ не дает, сами данные настолько большие, что их необходимо "перелить" из базы в сервис.

Здесь мы упираемся в сам факт соединения и получения данных.

2026-01-18T10:10:46.919+03:00  INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : Первый
Hibernate: 
            SELECT generate_series(1, 1000000) * 7 * 7 as num,
            CONCAT('Name ', cast (generate_series(0, 1000000 - 1) as text))  as name,
            CONCAT('Comment ', cast (generate_series(0, 1000000 - 1) as text))  as comment
        
2026-01-18T10:11:00.050+03:00  INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  Второй
Hibernate: 
            SELECT generate_series(1, 1000000) * 7 * 7 as num,
            CONCAT('Name ', cast (generate_series(0, 1000000 - 1) as text))  as name,
            CONCAT('Comment ', cast (generate_series(0, 1000000 - 1) as text))  as comment
        
2026-01-18T10:11:13.164+03:00  INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 26245 милисекунд на создание
2026-01-18T10:11:13.165+03:00  INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    :  
2026-01-18T10:11:13.165+03:00  INFO 11652 --- [nio-8080-exec-1] c.k.demo.service.TestCircleForService    : 27524

А на уровне самой базы (выборка через Dbeaver), результат естественно нормальный. Но учитываем, что там количество строк имеет естественное ограничение. При огромном числе нас будет ждать JdbcDriverError (намек на пагинацию).

Т.е., каждый раз будет возникать вопрос - что дороже, платить за вычисления в сервисе со своими данными, или брать из БД нужный преобразованный набор, но платить стоимостью коннекта и прочее.

Мое мнение - если подготовка данных (фильтрация, агрегация) имеет значительный эффект именно в БД (что происходит чаще), то лучше всегда сделать эти преобразования там, чем "забирать всё", а потом разгребать. Иными словами, если выборка из БД прямо обязательна, то мы все равно так или иначе платим за соединение, так что лучше преобразовать именно там + проверить скорость.

Что получили

Использование той или иной фичи/функциональности лежит на плечах разработчика.

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

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

Быстро преобразовать данные через map, filtes, foreach и др., и подставить в нужное место всегда выгодно с точки зрения компактности и аккуратности кода.

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

Параллельность и работа с данными извне также имеют свою стоимость.

Как применять эти результаты

Рецепты по оптимизации кода достаточно обширны и сильно зависят от конкретных ситуаций. Поэтому хочу написать общие соображения:

  • Стрелочные функции или лямбды хороши для коллекций небольших размеров. Преимущества - всегда под рукой и достаточно быстры, меньше кода.

  • Циклы. Недостатки - чуть больше кода. Преимущества: лучше читабельность, можно объединить общие итерации, когда это удобно, устранив дополнительные проходы.

Как оптимизировать?

  • Использовать переменные, если есть повторные вызовы стрелок, которые делают одно и тоже. НЕ ИСПОЛЬЗОВАТЬ дополнительные переменные, если есть отличия. Лучше несколько лямбд, которые делают немного разное, чем объединять всё в одну кучу и потом использовать if-else логику.

  • Если размер коллекции превышает 100к - 1M, то лучше сразу посмотреть на логику выше уровнем, скорее всего там что-то не так.

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

  • Выбирать правильную структуру данных для конкретных вещей. Чем лучше выбрана структура, тем меньше циклов и стрелочек придется писать.

  • Не копипастить чужой код

  • Оценивать производительность через исследование.

  • Изучать больше документацию.

  • И, конечно, код-ревью.

За скорость кодирования и красоту кода мы часто расплачиваемся ресурсами, снижением производительности и временем на исправление.

А что в итоге с водителем?

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