Frontend Evolution #2: Як війна браузерів народила динамічний веб, DHTML революція та Microsoft vs Netscape битва за DOM (1995-2000)

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

🎯 Від статичних сторінок до інтерактивних додатків

Пам’ятаю, як у 2008 році, починавши кар’єру в одній з європейських компанії, я вперше натрапив на legacy проект з DHTML кодом кінця 90-х. Там був величезний файл з визначенням типу браузера та подвійною реалізацією кожної функції — одна для IE, інша для Netscape. Коментарі в коді датувалися 1999 роком, а логіка була настільки заплутаною, що простіше було переписати все з нуля. Саме тоді я зрозумів, через яку болісну еволюцію пройшов веб: від статичних документів до динамічних додатків через епоху браузерних воєн та технологічної фрагментації.

У попередній статті ми розглянули, як 18 HTML-тегів заклали фундамент веба, але до середини 90-х їх обмеження стали критичними. Статичні документи більше не задовольняли потреби бізнесу та користувачів. Саме ці обмеження привели до народження Dynamic HTML (DHTML) — технології, що назавжди змінила наше розуміння того, що можна робити в браузері.

📚 Історичний контекст: коли статичність стала проблемою

Криза статичного веба

До 1997 року веб-розробка зводилася до створення статичних HTML-документів. Будь-яка інтерактивність означала повне перезавантаження сторінки — процес, що займав хвилини на повільних модемних з’єднаннях. Навіть проста валідація форми вимагала відправки даних на сервер через CGI-скрипти.

Бізнес вимагав більшого: миттєвої реакції на дії користувачів, динамічного оновлення контенту, складних інтерфейсів. Статичний HTML був як друкована книга — красиво, але абсолютно нерухомо.

Українські реалії: повільний Інтернет як каталізатор

В Україні кінця 90-х ситуація була особливо драматичною. Модемні з’єднання 14.4k та 28.8k робили кожне перезавантаження сторінки болісним випробуванням. Перші українські інтернет-провайдери з’явилися в 1995-1997 роках, і вартість підключення була астрономічною — $100-200 на місяць при середній зарплаті $50-80.

В таких умовах будь-яка технологія, що мінімізувала серверні запити, була на вагу золота. Саме тому українські веб-студії активно експериментували з ранніми формами динамічності.

Війна браузерів як каталізатор інновацій

1995-2001 роки увійшли в історію як перша «Війна браузерів» між Netscape Navigator та Microsoft Internet Explorer. Netscape контролював 80% ринку, але Microsoft, користуючись монополією Windows, агресивно просувала IE.

Ключові віхи протистояння:

  • 1995 — JavaScript від Netscape vs JScript від Microsoft
  • 1996 — CSS 1.0 з різними інтерпретаціями
  • 1997 — рік народження DHTML: IE 4.0 vs Navigator 4.0
  • 1998 — пік нововведень та критичної несумісності

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

⚙️ Технічна архітектура: два світи DHTML

Internet Explorer: революція document.all

Microsoft з IE 4.0 запропонувала радикальну ідею: кожен елемент HTML-документа має бути доступний для маніпуляцій через JavaScript. Це було реалізовано через революційну колекцію document.all:

// IE 4.0+ - доступ до будь-якого елемента
function changeContent() {
    // Доступ за ID
    document.all["myDiv"].innerHTML = "Новий контент!";

    // Зміна стилів
    document.all["myDiv"].style.color = "red";
    document.all["myDiv"].style.fontSize = "20px";

    // Динамічна зміна позиції
    document.all["myDiv"].style.position = "absolute";
    document.all["myDiv"].style.left = "100px";
    document.all["myDiv"].style.top = "50px";
}

// Революційна можливість - innerHTML
function updatePage() {
    document.all["content"].innerHTML = 
        "<h2>Динамічно створений заголовок</h2>" +
        "<p>Цей контент з'явився без перезавантаження!</p>";
}

Visual Filters: Photoshop у браузері

Microsoft пішов далі, інтегрувавши в IE 4.0 Visual Filters — технологію, що дозволяла застосовувати Photoshop-подібні ефекти до HTML-елементів:

// IE Visual Filters - справжня магія 1997 року!
function applyEffects() {
    // Розмиття
    document.all["myDiv"].style.filter = "blur(strength=5)";

    // Прозорість з градієнтом
    document.all["header"].style.filter = 
        "alpha(opacity=50) gradient(startColorstr='#FF0000', endColorstr='#0000FF')";

    // Тінь
    document.all["text"].style.filter = "dropshadow(color='black', offx=3, offy=3)";

    // Світіння
    document.all["button"].style.filter = "glow(color='yellow', strength=3)";
}

Netscape Navigator: філософія Layers

Netscape обрала принципово інший підхід. Замість універсального доступу до всіх елементів, вони створили концепцію «слоїв» (Layers) — спеціальних контейнерів для динамічного контенту:

// Netscape 4.x - тільки абсолютно позиціоновані елементи
function moveLayer() {
    // Доступ тільки до layer'ів
    document.layers["myLayer"].left = 200;
    document.layers["myLayer"].top = 100;
    document.layers["myLayer"].visibility = "show";
}

// Детальніше про Netscape Layers API
var myLayer = document.layers["gameLayer"];

// Позиціонування
myLayer.moveTo(100, 200);           // Абсолютна позиція
myLayer.moveBy(50, -30);            // Відносне переміщення
myLayer.moveToAbsolute(300, 400);   // Відносно всієї сторінки

// Z-index маніпуляції
myLayer.moveAbove(document.layers["background"]);
myLayer.moveBelow(document.layers["foreground"]);

// Розміри та відсікання
myLayer.resizeTo(200, 150);
myLayer.resizeBy(20, 10);
myLayer.clip.top = 10;
myLayer.clip.right = 190;

// Динамічне завантаження контенту
myLayer.src = "external_content.html";
myLayer.load("dynamic_page.html", 300);  // з заданою шириною

// Видимість
myLayer.visibility = "hide";  // "show" | "hide" | "inherit"

JavaScript Style Sheets (JSS): альтернатива CSS

Окрім layers, Netscape запропонувала революційну альтернативу CSS — JavaScript Style Sheets (JSS). Замість декларативного CSS синтаксису, JSS дозволяв описувати стилі через JavaScript:

// JSS - JavaScript Style Sheets (тільки Navigator 4)
tags.H1.color = "blue";
tags.H1.fontSize = "24pt";
tags.P.marginLeft = "20px";

// Умовні стилі
if (navigator.appName == "Netscape") {
    tags.BODY.backgroundColor = "lightblue";
} else {
    tags.BODY.backgroundColor = "white";
}

// Класи через JSS
classes.highlight.P.backgroundColor = "yellow";
classes.highlight.P.fontWeight = "bold";

JSS виглядав потужно для програмістів, але мав фатальну ваду — підтримувався тільки Navigator 4 і ніколи не став стандартом.

VBScript: Microsoft альтернатива JavaScript

Microsoft також просувала VBScript як альтернативу JavaScript, особливо для корпоративних клієнтів:

' VBScript - тільки Internet Explorer
Sub ChangeContent()
    Document.All("myDiv").innerHTML = "Оновлено через VBScript!"
    Document.All("myDiv").Style.Color = "red"
End Sub

' Обробка подій
Sub Button1_OnClick()
    MsgBox "Кнопка натиснута!"
    Document.All("result").innerHTML = "VBScript працює!"
End Sub

Це створювало ще більшу фрагментацію — тепер розробники мали вибирати між JavaScript (кроссбраузерний) та VBScript (тільки IE, але інтегрований з Windows).

Архітектурні відмінності

Різниця була фундаментальною. Ось як виглядали ключові аспекти в кожному браузері:

🌐 Internet Explorer 4.0+

// ✅ Доступ до будь-якого елемента
document.all["anyElement"].innerHTML = "Новий контент";
document.all["anyElement"].style.color = "red";

// ⚡ Швидка зміна контенту
document.all["content"].innerHTML = "Миттєве оновлення";

// 🎨 Повний контроль CSS
document.all["box"].style.fontSize = "20px";
document.all["box"].style.backgroundColor = "blue";

// 🌊 Event bubbling
element.onclick = function() { /* подія спливає вгору */ };

// 🎭 Visual Filters (унікальна фішка IE)
document.all["text"].style.filter = "glow(color='yellow', strength=3)";

📦 Netscape Navigator 4.x

// ❌ Доступ тільки до layers
document.layers["myLayer"].left = 100;
document.layers["myLayer"].visibility = "show";

// 🐌 Повільна зміна контенту
var layer = document.layers["myLayer"];
layer.document.open();
layer.document.write("Треба відкрити/закрити");
layer.document.close();

// 🔒 Обмежені CSS можливості
layer.left = 50;        // ✅ Можна
layer.visibility = "hide"; // ✅ Можна
// layer.style.fontSize — ❌ Неможливо!

// 🎯 Event capture тільки на layers
layer.captureEvents(Event.MOUSEOVER);
layer.onmouseover = handleEvent;

// 💻 JavaScript Style Sheets (унікальна фішка Netscape)
tags.H1.color = "blue";
tags.P.fontSize = "14pt";

🔍 Ключові філософські відмінності:

Internet Explorer: «Кожен HTML-елемент — це програмний об’єкт»

  • Універсальний доступ до DOM
  • Повна свобода маніпуляцій

Netscape Navigator: «Слоєва архітектура для позиціонованого контенту»

  • Тільки спеціальні контейнери
  • Фокус на позиціонуванні та анімації

💼 Практичні кейси: реальність розробки того часу

Проект: модернізація банківського порталу (2009)

Працюючи над оновленням банківського порталу для європейського клієнта, ми зіткнулися з класичним legacy кодом DHTML епохи. Система була написана в 2000-2001 роках і містила всі «красоти» того періоду — подвійні реалізації для IE/Netscape, визначення типу браузера та заплутану логіку динамічних інтерфейсів.

Результатом був код-монстр з визначенням типу браузера:

// Визначення типу браузера - щоденна реальність 1999 року
var isIE = document.all ? true : false;
var isNetscape = document.layers ? true : false;

function showBalance(amount) {
    if (isIE) {
        // IE версія
        document.all["balance"].innerHTML = 
            "<strong>₴" + amount + "</strong>";
        document.all["balance"].style.color = "green";
    } else if (isNetscape) {
        // Netscape версія
        var layer = document.layers["balance"];
        layer.document.open();
        layer.document.write(
            "<font color='green'><b>₴" + amount + "</b></font>"
        );
        layer.document.close();
    }
}

// Dropdown меню - подвійна реалізація
function toggleMenu(menuId) {
    if (isIE) {
        var menu = document.all[menuId];
        menu.style.display = 
            menu.style.display === "none" ? "block" : "none";
    } else if (isNetscape) {
        var menu = document.layers[menuId];
        menu.visibility = 
            menu.visibility === "hide" ? "show" : "hide";
    }
}

Уроки, які я виніс з вивчення legacy коду:

  1. Подвійна розробка — кожну функцію доводилося писати двічі
  2. Визначення браузера як обов’язкова практика того часу
  3. Degradation strategy — сайт мав працювати навіть без JS
  4. User education — інструкції «Завантажте IE 4+» на старих сайтах

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

Кейс: навчання через legacy код (2012-2013)

Працюючи з різними CMS та legacy проектами, я часто натрапляв на коди DHTML епохи. Особливо цікавим був один Drupal-проект 2002 року з модулем динамічного меню, написаним у стилі тих часів:

// Реальний код з WordPress плагіна ~2003 року
function wpMenuDHTML() {
    if (document.all) {
        // IE branch
        for(var i = 0; i < document.all.tags("LI").length; i++) {
            if(document.all.tags("LI")[i].className == "menu-item") {
                document.all.tags("LI")[i].onmouseover = function() {
                    this.style.backgroundColor = "#cccccc";
                    if(this.children[1]) {
                        this.children[1].style.display = "block";
                    }
                };
            }
        }
    } else if (document.layers) {
        // Netscape branch (вже не актуальний у 2003, але залишався в коді!)
        for(var i = 0; i < document.layers.length; i++) {
            if(document.layers[i].id.indexOf("menu") > -1) {
                document.layers[i].captureEvents(Event.MOUSEOVER);
                document.layers[i].onmouseover = showSubmenu;
            }
        }
    }
}

Досвід з legacy CMS та DHTML артефактами

Я регулярно натрапляв на різні подібні «артефакти»:

Magento 1.x legacy модулі:

// Реальний код з Magento модуля ~2004 року
function MagentoProductTabs() {
    // Стара школа DHTML
    if (document.all) {
        // IE версія
        this.tabs = document.all.tags('DIV');
        this.showTab = function(tabId) {
            for(var i = 0; i < this.tabs.length; i++) {
                if(this.tabs[i].className.indexOf('product-tab') > -1) {
                    this.tabs[i].style.display = 'none';
                }
            }
            document.all[tabId].style.display = 'block';
        };
    } else if (document.layers) {
        // Netscape - навіть у 2004!
        this.showTab = function(tabId) {
            for(var layerId in document.layers) {
                if(layerId.indexOf('tab') > -1) {
                    document.layers[layerId].visibility = 'hide';
                }
            }
            document.layers[tabId].visibility = 'show';
        };
    }
}

Альтернативи DHTML: Flash та перші скрипти

Паралельно з DHTML розвивалася інша технологія — Macromedia Flash. Цікаво, що на початку Flash мав лише примітивні «Actions» (Flash Player 4, 1999), а повноцінний ActionScript 1.0 з’явився тільки в 2000 році з Flash Player 5:

// Flash Actions (1999) - примітивні команди
gotoAndPlay(1);
gotoAndStop(10);
nextFrame();

// ActionScript 1.0 (2000) - вже справжня мова програмування
function updateBalance(amount) {
    // Flash виконував цей код швидше за JavaScript того часу
    balanceText.text = "$" + amount;
    balanceText._alpha = 0;

    // Анімація через timeline
    for(var i = 0; i <= 100; i++) {
        balanceText._alpha = i;
    }
}

Порівняння Flash vs DHTML (2000+ роки):

🌐 DHTML (з 1997 року)

  • ⚡ Швидкість: Інтерпретований JavaScript — повільніше
  • 🌍 Сумісність: Браузерні війни — код для IE ≠ код для Netscape
  • 📦 Розмір: Легкі HTML/CSS/JS файли — швидке завантаження
  • 🔍 SEO: Повністю індексується пошуковиками
  • 💰 Вартість: Безкоштовно, відкриті стандарти
  • 🛠️ Інструменти: Текстовий редактор, прості IDE

🎬 Flash ActionScript (з 2000 року)

  • ⚡ Швидкість: Компільований байт-код — значно швидше
  • 🌍 Сумісність: Однаковий код працює скрізь через Flash Player
  • 📦 Розмір: «Важкі» SWF файли — повільне завантаження
  • 🔍 SEO: Чорна скринька для пошуковиків — контент невидимий
  • 💰 Вартість: Платні Adobe інструменти (Flash Professional)
  • 🛠️ Інструменти: Спеціалізовані IDE, timeline editor

🎯 Практичні наслідки:

Коли обирали DHTML:

  • SEO-критичні сайти (магазини, корпоративні)
  • Швидке завантаження на повільному інтернеті
  • Обмежений бюджет на інструменти

Коли обирали Flash:

  • Ігри та мультимедійний контент
  • Складна анімація та візуальні ефекти
  • Одноразові рекламні банери
  • Інтерактивні презентації та портфоліо

Ember.js як еволюція DHTML підходів

Пізніше, працюючи з Ember.js, я зрозумів, як концепції DHTML еволюціонували. Ember’s convention-over-configuration нагадував структурований підхід до організації DHTML коду:

// Ember.js - еволюція DHTML організації
App.MenuComponent = Ember.Component.extend({
  // Lifecycle methods - еволюція DHTML init/destroy
  didInsertElement: function() {
    this.initDynamicBehavior();
  },

  willDestroyElement: function() {
    this.cleanup(); // Memory management як в DHTML
  },

  actions: {
    toggleSubmenu: function() {
      // Event handling - прямий спадкоємець DHTML events
      this.toggleProperty('isVisible');
    }
  }
});

🌱 Спадок та вплив на сучасну розробку

Архітектурні паттерни DHTML епохи та їх спадок

DHTML епоха породила фундаментальні архітектурні підходи, які живуть досі:

1. Component-based Architecture

// DHTML підхід - 1998 рік
function MenuComponent(containerId) {
    this.container = document.all ? 
        document.all[containerId] : 
        document.layers[containerId];

    this.show = function() { /* логіка показу */ };
    this.hide = function() { /* логіка приховування */ };
    this.destroy = function() { /* cleanup */ };
}

// Сучасний React - 2025 рік  
function MenuComponent({ isVisible, onClose }) {
    return isVisible ? <div>Menu content</div> : null;
}

2. State Management Pattern

// DHTML State Manager - прототип Redux
var AppState = {
    currentMenu: null,
    userLoggedIn: false,

    setState: function(newState) {
        // Примітивний state update
        for(var prop in newState) {
            this[prop] = newState[prop];
        }
        this.render();
    },

    render: function() {
        // Перерендер UI на основі state
        if(this.userLoggedIn) {
            showUserMenu();
        } else {
            showLoginForm();
        }
    }
};

3. Event-Driven Architecture

// DHTML Events - народження паттерну
function EventManager() {
    this.listeners = {};

    this.on = function(event, callback) {
        if(!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
    };

    this.emit = function(event, data) {
        if(this.listeners[event]) {
            for(var i = 0; i < this.listeners[event].length; i++) {
                this.listeners[event][i](data);
            }
        }
    };
}

// Сучасний EventEmitter - той самий принцип!

4. Dependency Injection зародки

// DHTML "DI Container" - 1999 рік
var ServiceContainer = {
    services: {},

    register: function(name, factory) {
        this.services[name] = factory;
    },

    get: function(name) {
        return this.services[name]();
    }
};

// Реєстрація сервісів
ServiceContainer.register('httpService', function() {
    return new XMLHttpRequest(); // коли з'явився!
});

Performance-first thinking — DHTML змусив розробників думати про перформанс з самого початку:

// DHTML підхід - мінімізація DOM операцій
var fragment = document.createDocumentFragment();
for(var i = 0; i < items.length; i++) {
    var li = document.createElement('li');
    li.innerHTML = items[i];
    fragment.appendChild(li);  // Не торкаємось DOM!
}
list.appendChild(fragment);  // Одна DOM операція

// React Virtual DOM - та сама ідея, але краще
function ItemList({ items }) {
    return (
        <ul>
            {items.map(item => <li key={item.id}>{item.text}</li>)}
        </ul>
    ); // React оптимізує DOM операції
}

Graceful Degradation принцип:

// DHTML підхід
if(document.all || document.layers) {
    // Розширена функціональність
    initDynamicMenu();
} else {
    // Базова функціональність
    showStaticMenu();
}

// Сучасний підхід  
if('IntersectionObserver' in window) {
    // Прогресивна функціональність
    initLazyLoading();
} else {
    // Fallback
    loadAllImages();
}

Cross-platform compatibility thinking:

// DHTML legacy
function createElement(tag, props) {
    var element;
    if(document.createElement) {
        element = document.createElement(tag);
    } else {
        // IE5 fallback
        element = document.all.tags(tag)[0].cloneNode(false);
    }
    return element;
}

// Сучасні polyfills - та сама ідея
if(!Array.prototype.includes) {
    Array.prototype.includes = function(searchElement) {
        return this.indexOf(searchElement) !== -1;
    };
}

Вплив на сучасні архітектури

Micro-frontends — прямий спадкоємець DHTML багатофайлової архітектури:

  • DHTML: різні .js файли для різних частин сайту
  • Зараз: різні бандли для різних частин додатку

Component lifecycle — концепція з DHTML:

  • DHTML: init(), show(), hide(), destroy()
  • React: componentDidMount(), componentWillUnmount(), etc.

Framework-agnostic thinking — DHTML навчив писати код, що працює скрізь:

// DHTML wrapper - прообраз jQuery
var DOM = {
    get: function(id) {
        return document.all ? document.all[id] : document.getElementById(id);
    },

    addClass: function(element, className) {
        if(element.className) {
            element.className += ' ' + className;
        } else {
            element.className = className;
        }
    }
};

// jQuery - еволюція тієї ж ідеї
$('#element').addClass('active');

Performance Lessons: від DHTML до сучасних SPA

Робота з trading системами навчила мене, що принципи оптимізації DHTML епохи актуальні досі:

// DHTML "батчинг" DOM операцій - 1998
function updateMultipleElements(updates) {
    // Збираємо всі зміни
    var changes = [];
    for(var i = 0; i < updates.length; i++) {
        changes.push({
            element: document.all[updates[i].id],
            property: updates[i].property,
            value: updates[i].value
        });
    }

    // Застосовуємо за один прохід
    for(var j = 0; j < changes.length; j++) {
        changes[j].element[changes[j].property] = changes[j].value;
    }
}

// React Concurrent Features - та сама ідея
function TradingWidget({ data }) {
    return (
        <React.Fragment>
            {data.map(item => (
                <PriceRow key={item.id} price={item.price} />
            ))}
        </React.Fragment>
    ); // React автоматично батчить оновлення
}

Memory Management — критично важливо було в DHTML:

// DHTML cleanup pattern
function destroyWidget() {
    // Видаляємо event listeners
    element.onclick = null;
    element.onmouseover = null;

    // Очищаємо посилання
    element.parentNode.removeChild(element);
    element = null;

    // Garbage collection hint
    if(window.CollectGarbage) {
        window.CollectGarbage(); // IE specific
    }
}

Ці паттерни не стали основою для React’s useEffect cleanup та Vue’s beforeDestroy lifecycle методів. Але схожі проблеми призвели до схожих рішень.

Cross-browser compatibility — урок DHTML епохи:

  • Тоді: подвійний код для IE/Netscape з перевіркою типу браузера
  • Зараз: Babel, polyfills, autoprefixer

Але DHTML мав критичні обмеження: відсутність стандартизації призводила до фрагментації веба, а пряма маніпуляція DOM була повільною та неефективною для складних інтерфейсів. До 2000 року стало зрозуміло, що потрібні стандарти. Ці проблеми стали каталізатором для наступної революції — появи W3C DOM стандартів, CSS 2.1, стандартизації ECMAScript та народження концепції XMLHttpRequest (яка трохи пізніше переросте в Ajax), що дозволила створювати справді динамічні веб-додатки без постійних перезавантажень сторінок.

🎯 Висновки та перспективи

DHTML епоха (1995-2000) була періодом болісного, але революційного дорослішання веба:

Народження інтерактивності — революційний перехід від статичних документів до динамічних додатків

DOM як програмна концепція — кожен елемент сторінки вперше став програмним об’єктом

Event-driven архітектура — реакція на дії користувача без перезавантаження (прообраз сучасних SPA)

Performance-first thinking — DHTML змусив думати про оптимізацію з першого дня

Component paradigm — від Netscape layers до React компонентів

Cross-platform compatibility — зародження підходів до кроссбраузерної розробки

Архітектурні уроки для сучасності:

  • State management — централізоване управління станом додатку
  • Lifecycle patterns — init/show/hide/destroy → mount/update/unmount
  • Dependency injection — раннє розуміння важливості DI контейнерів
  • Memory management — культура cleanup’у та уникнення memory leaks
  • Progressive enhancement — graceful degradation як основа accessibility

Ключовий takeaway: технологічна конкуренція може стимулювати революційні інновації, але відсутність стандартів створює неприйнятну фрагментацію екосистеми. DHTML показав потужність динамічного веба та заклав архітектурні основи сучасних фреймворків, але також продемонстрував критичну важливість галузевого консенсусу в технологічних рішеннях.

Український контекст: В умовах повільного та дорогого Інтернету 90-х, DHTML технології були особливо цінними, дозволяючи мінімізувати серверні запити та створювати «розумні» інтерфейси. Цей досвід сформував культуру оптимізації, що залишається актуальною досі.

Війна браузерів закінчилася перемогою стандартів: W3C DOM, CSS 2.1 та ECMAScript створили фундамент для наступного етапу. Про те, як ці стандарти привели до народження XMLHttpRequest/Ajax, появи перших JavaScript-бібліотек (Prototype, jQuery) та формування екосистеми, яка зробила веб-розробку знову передбачуваною та масштабованою, читайте в наступній статті циклу.

Корисні ресурси:

Питання для обговорення: Чи стикалися ви з legacy кодом DHTML епохи? Які архітектурні паттерни того часу ви помічаєте в сучасних фреймворках? Як досвід повільного Інтернету 90-х вплинув на культуру оптимізації в Україні? Поділіться досвідом у коментарях! 👇

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі