Optymalizacja kodu .NET (a konkretnie ASP.NET MVC) z mojej perspektywy

By | 18 września 2018

Pisanie optymalnego i przejrzystego kodu jest bardzo trudną sztuką, której można się uczyć bez końca a i tak znajdzie się ktoś kto powie że można lepiej (a kto Panu tak s***lił). Tak więc nie będę się chwalił jakim to świetnym programistą jestem(bo nadal pewnie jestem marnym)  tylko podam Wam kilka przykładów zmian, które w przypadku projektów w których uczestniczę przyniosły dobry efekt. Pewnie można by było zamiast jednego obszernego wpisu zrobić całą serię omawiającą wiele technik, ale nie wiem czy będę miał tyle motywacji więc niczego nie obiecuję tylko zapraszam do lektury.

Nie zwracajcie uwagę na kolejność tych zagadnień, bo nie są one ułożone wg. ważności, tylko zaczynam od tych o których najprościej mi się pisze, a później będzie już kosmos 🙂

Array vs List?
Mam wrażenie, że każdy kogo pytam ma na to swoje zdanie, tak więc dołożę też swoje 🙂 Wszystko zależy w jakich sytuacjach będziemy je stosować, jeżeli zamierzasz wyciągnąć z bazy dane i tylko wyświetlić je na stronie, nie wykonując żadnych dodatkowych operacji, to myślę że najlepszym rozwiązaniem będzie użycie tablicy zamiast listy. Jednak jeżeli zamierzasz wykonywać operacje typu dodawanie, usuwanie elementów to skorzystaj z listy. Klasa listy zapewnia metody i właściwości, jak inne klasy kolekcji, takie jak dodawanie, wstawianie, usuwanie, wyszukiwanie, itp. Jest to zamiennik dla tablic, połączonych list, kolejek, itp. Jest lepsza od tablicy ponieważ potrafi dużo szybciej obsługiwać operacje zmiany rozmiaru.

Nie ukrywam że przez swoje lenistwo i brak wiedzy przez wiele lat używałem bezmyślnie list do przechowywania wszystkiego czego się da i nie zwracałem uwagi na to że w jakimś momencie może to być kompletnie nieoptymalne, ale teraz staram się myśleć do czego będę potrzebował obiekty wyciągane np. z bazy danych. Jeżeli nie zamierzam wykonywać skomplikowanych operacji to od razu wybieram Array i choć efekty są niewielkie (pewnie rzędu 1% przy większych kolekcjach) uważam że nie jest wielkim utrudnieniem żeby z tego korzystać.

Nie używaj ToLower jeżeli masz bazę danych/tabelę z CI (Case Insensitive)
CS (Case-Sensitive) i CI (Case-Insensitive) odpowiedzialne odpowiednio za rozróżnianie i nierozróżnianie dużych i małych liter przy operacjach na tekstach (sortowanie, porównywanie, itp.) w bazie danych. Tak więc jeżeli nie chcesz spowolnić działania swojej aplikacji, to zamiast używać w Linq to SQL przy porównywaniu ToLower() dla obu elementów porównania, olej to i skorzystaj z właściwości CI. Case Insensitive możesz ustawić w bazie danych/tabeli lub nawet pojedynczym polu tabeli. Serio to daje na prawdę ogromny przyrost wydajności a koszt programistyczny jest tak niewielki, że aż szkoda gadać.

Asynchroniczne Linq
Sama asynchroniczność to temat rzeka i nie jestem zdecydowanie osobą która powinna się wypowiadać na temat poprawnego użycia takiej czy innej metody do rozwiązania konkretnego problemu, ale mogę opowiedzieć o konkretnym rozwiązaniu które w przypadku ASP.NET MVC rzeczywiście powoduje zwiększenie wydajności.

Z linq korzystam już od wielu lat i uważam że jest to najlepsze co w programowaniu mnie spotkało, ponieważ z jego użyciem w tak prosty i logiczny sposób można wykonywać operacje na kolekcjach a jeszcze do tego wsparcie EntityFramework. Po prostu bajka ! Nie no ok, przesadzam są zapewne lepsze i bardziej wydajne rozwiązania, ale po prostu ja lubię Linq i ostatnio zacząłem korzystać z fajnej właściwości która była w Linq od dawna ale ja nie potrafiłem z niej korzystać. A mianowicie Linq ma metody asynchroniczne.

No ale co z tego wynika? A no to, że można cięższe operacje na kolekcjach przenieść na początek wykonywania kodu i dopiero w momencie gdy dane stają się potrzebne za pomocą komendy await poczekać na wyniki (a już część innego kodu się wykonała, czyli jesteśmy do przodu). Jednak ponieważ w większości korzystam z Linq do EntityFramework to akurat ta właściwość nie jest idealna, bo okazuje się że w tym wypadku niestety nie da się zrobić dwóch wywołań do bazy na jednym kontekście w jednej chwili. Tak więc aby móc odpalić asynchronicznie wyciągnięcie dwóch list z bazy trzeba tworzyć dwa konteksty, a to już nie jest ładne rozwiązanie i nie polecam. Ale jeżeli mamy do wyciągnięcie jedną większą listę to jak najbardziej można zrobić to asynchonicznie i jakiś zysk z tego będzie.

Asynchroniczne Linq z możliwością jego anulowania
Jednak rozszerzenie asynchronicznego Linq o możliwość jego anulowania w trakcie działania dało realny przypływ wydajności w aplikacji którą tworzę. Tutaj dobry jest przykład. Jeżeli klient wchodzi na stronę i przeklikuje się przez różne strony jeszcze zanim załadują się do końca lub zamyka stronę po kliknięciu linka, to ten request który poszedł do serwera jest przetwarzany i operacje na bazie czy też skomplikowane query nadal się dzieją co nie jest zbyt optymalne. A poniższe rozwiązanie ma na celu zatrzymanie wykonania query w przypadku otrzymania informacji o rozłączeniu się klienta lub anulowania wykonywania operacji przez przeglądarkę:

public async Task GetData(CancellationToken cancellationToken)
{
    CancellationToken disconnectedToken = Response.ClientDisconnectedToken;
var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, disconnectedToken);
var dictionary = await db.Users.ToDictionaryAsync(x => x.UserId, ..., source.Token);
}

Kod jest mega prosty a daje na prawdę dużo, ale polecam to sprawdzić na własną rękę.

Mówią że samo dodanie do metody w kontrolerze ASP.NET właściwości async podobno poprawia działanie, ale co do tego bym nie był takim optymistą, moim zdaniem właśnie użycie powyższego rozwiązania daje efekt przyspieszenia działania aplikacji, ale to musicie sprawdzić sami.

Zamiast pobierania całego obiektu z bazy danych użyj Select z Linq to SQL i wypełnij obiekt wybranymi danymi
Linq ze względu na swoją specyfikę bardzo ułatwia pobieranie obiektów z bazy danych jednocześnie z ich relacjami. Jednak jest to mega nieefektywne, ponieważ często dane, które pochodzą z relacji są nam zwyczajnie niepotrzebne i niepotrzebnie wydłużają czas pobrania danych. Jest na to wiele rozwiązań ale ja polecam jedno (wg. mnie najwygodniejsze), czyli stworzenie sobie klasy(ewentualnie użycie dictionary), która będzie zawierała tylko te pola które są nam aktualnie potrzebne do wykonania zadania. Następnie za pomocą funkcji Select() wybranie które dane trafią do jakiego pola. Poniższy kod to świetnie ilustruje:

var model = await db.CustomerSet
    .OrderBy(x => x.Code)
    .Select(x => new CustomerDTOViewModel()
    {
        Id = x.Id,
        Code = x.Code,
        Name = x.Name,
        NipOrPesel = x.NIP,
        Address = x.Address,
        City = x.City,
        PostCode = x.PostCode,
        AnyWork = x.Work.Any(),
        Active = x.Active
    }).ToListAsync(source.Token);

Dzięki takiem rozwiązaniu zaoszczędzimy czas na pobranie wszystkich obiektów z tabeli Work, które są w relacji do klienta, tylko po to żeby sprawdzić czy nie ma żadnej pracy. Jest to dużo bardziej efektywne, a w przypadku ASP.NET MVC taki model dużo łatwiej wyświetlić na stronie, a kod jest dużo czystszy zakładając że nie będziemy jeszcze z tymi danymi kombinować na widoku czego nie jestem zwolennikiem, w końcu nie bez powodu mamy MVC żeby oddzielać widok od logiki i danych.

Tutaj muszę jednak dodać że jeżeli jednak uprzemy się żeby nie tworzyć modeli pośrednich, to można użyć poniższego kodu, żeby ograniczyć liczbę ładowanych relacji podczas pobierania z SQL, ale i tak ilość nadmiarowych danych będzie dużo większa niż w przypadku modelu pośredniego.

db.Configuration.LazyLoadingEnabled = false;
db.Configuration.ProxyCreationEnabled = false;

Niezależne od siebie elementy doładowuj AJAXem
Jeżeli masz na jednej stronie kilka elementów, które są bardzo obciążające ładowanie strony a między nimi nie dochodzi do zależności, które trzeba by było w niebanalny sposób rozwiązywać, to najlepszym pomysłem byłoby rozbić pobieranie tych elementów na osobne requesty, które doładują się w różnym czasie ale dzięki temu klient zobaczy już pierwsze działające elementy strony. Oczywiście cokolwiek się robi, trzeba to robić z głową, więc tutaj też nie zachęcam do serwowania stron które będą się doładowywały klientowi przesuwając elementy strony i doprowadzając do szewskiej pasji. Chodzi mi o to, że jeżeli na stronie jest główny element po który klient wchodzi na nią a dodatkowo musi się pojawić gdzieś kalendarz z wydarzeniami, to po co czekać aż cały kalendarz się załaduje razem ze stroną, jak można go doładować w trakcie jak klient przegląda główną treść strony.

CDN.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *