Pomimo licznych zalet, Ruby on Rails jest nadal uważany za stosunkowo powolny framework webowy. Wszyscy wiemy, że Twitter porzucił Railsy na rzecz Scali. Jednak dzięki kilku sprytnym ulepszeniom możesz uruchomić swoją aplikację znacznie szybciej!
Ruby First
Ruby jest językiem silnie zorientowanym obiektowo. W rzeczywistości (prawie) wszystko w Ruby jest obiektem. Tworzenie niepotrzebnych obiektów może kosztować program wiele dodatkowej pamięci, więc należy tego unikać.
Aby zmierzyć różnicę, użyjemy memory_profiler oraz wbudowany moduł Benchmark do pomiaru wydajności czasowej.
Używanie metod bang! na łańcuchach
require "memory_profiler"
report = MemoryProfiler.report do
data = "X" * 1024 * 1024 * 100
data = data.downcase
end
report.pretty_print
W poniższym zestawieniu utworzyliśmy ciąg znaków o rozmiarze 100 MB i zmniejszyliśmy każdy zawarty w nim znak. Nasz test porównawczy daje nam następujący raport:
Całkowita alokacja: 210765044 bajtów (6 obiektów)
Jeśli jednak zastąpimy linię 6 przez:
data.downcase!
Odczytywanie plików wiersz po wierszu
Przypuszczalnie musimy pobrać ogromną kolekcję danych zawierającą 2 miliony rekordów z pliku csv. Zazwyczaj wygląda to następująco:
require 'benchmark'
Benchmark.bm do |x|
x.report do
File.readlines("2mrecords.csv").map! {|line|line.split(",")}
end
end
użytkownik system całkowity rzeczywisty
12.797000 2.437000 15.234000 (106.319865)
Pełne pobranie pliku zajęło nam ponad 106 sekund. Całkiem sporo! Możemy jednak przyspieszyć ten proces, zastępując plik mapa! z prostą metodą podczas gdy pętla:
require 'benchmark'
Benchmark.bm do |x|
x.report do
file = file.open("2mrecords.csv", "r")
while line = file.gets
line.split(",")
end
end
end
użytkownik system całkowity rzeczywisty
6.078000 0.250000 6.328000 ( 6.649422)
Czas działania drastycznie się skrócił od czasu premiery mapa! metoda należy do określonej klasy, np. Hash#map lub Array#mapgdzie Ruby będzie przechowywać każdą linię przeanalizowanego pliku w pamięci tak długo, jak będzie wykonywana. Odśmiecacz Rubiego nie zwolni pamięci przed pełnym wykonaniem tych iteratorów. Jednak odczytanie go wiersz po wierszu spowoduje, że GC przeniesie pamięć z poprzednich wierszy, gdy nie będzie to konieczne.
Unikanie iteratorów metod na większych kolekcjach
Jest to rozszerzenie poprzedniego punktu o bardziej powszechny przykład. Jak już wspomniałem, Ruby iteratory są metodami obiektowymi i nie zwalniają pamięci, dopóki są wykonywane. W małej skali różnica jest bez znaczenia (a metody takie jak mapa wydaje się bardziej czytelna). Jednak w przypadku większych zestawów danych zawsze warto rozważyć zastąpienie go bardziej podstawowymi pętlami. Tak jak w poniższym przykładzie:
numberofelements = 10000000
randoms = Array.new(numberofelements) { rand(10) }
randoms.each do |line|
#do something
end
i po refaktoryzacji:
numberofelements = 10000000
randoms = Array.new(numberofelements) { rand(10) }
while randoms.count > 0
line = randoms.shift
#do something
end
"`
Użyj metody String::<<
Jest to szybka, ale szczególnie przydatna wskazówka. Jeśli dodasz jeden ciąg do drugiego za pomocą operatora += za kulisami. Ruby utworzy dodatkowy obiekt. Więc to:
a = "X"
b = "Y"
a += b
W rzeczywistości oznacza to:
a = "X"
b = "Y"
c = a + b
a = c
Operator mógłby tego uniknąć, oszczędzając trochę pamięci:
a = "X"
b = "Y"
a << b
Porozmawiajmy o Railsach
The Framework Rails posiada mnóstwo "gotchas", które pozwoliłyby zoptymalizować kod szybko i bez dodatkowego wysiłku.
Eager Loading AKA problem n+1 zapytań
Załóżmy, że mamy dwa powiązane modele, Post i Author:
class Autor < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :author
end
Chcemy pobrać wszystkie posty w naszym kontrolerze i wyrenderować je w widoku wraz z ich autorami:
kontroler
def index
@posts = Post.all.limit(20)
end
widok
W kontrolerze, ActiveRecord utworzy tylko jedno zapytanie, aby znaleźć nasze posty. Ale później uruchomi również kolejne 20 zapytań, aby odpowiednio znaleźć każdego autora - zajmując dodatkowy czas! Na szczęście Railsy oferują szybkie rozwiązanie pozwalające połączyć te zapytania w jedno. Używając funkcji zawiera możemy przepisać nasz kontroler w ten sposób:
def index
@posts = Post.all.includes(:author).limit(20)
end
Na razie tylko niezbędne dane będą pobierane w jednym zapytaniu.
Można również użyć innych klejnotów, takich jak kula aby dostosować cały proces.
Dzwoń tylko do tego, czego potrzebujesz
Inną przydatną techniką zwiększania szybkości ActiveRecord jest wywoływanie tylko tych atrybutów, które są niezbędne do bieżących celów. Jest to szczególnie przydatne, gdy aplikacja zaczyna się rozrastać, a liczba kolumn w tabeli również wzrasta.
Weźmy nasz poprzedni kod jako przykład i załóżmy, że musimy tylko wybrać nazwiska z autorów. Możemy więc przepisać nasz kontroler:
def index
@posts = Post.all.includes(:author).select("name").limit(20)
end
Teraz instruujemy nasz kontroler, aby pomijał wszystkie atrybuty z wyjątkiem tego, którego potrzebujemy.
Prawidłowe renderowanie częściowych
Załóżmy, że chcemy utworzyć osobną część dla naszych postów z poprzednich przykładów:
Na pierwszy rzut oka kod ten wygląda poprawnie. Jednak przy większej liczbie postów do wyrenderowania, cały proces będzie znacznie wolniejszy. Dzieje się tak, ponieważ Szyny ponownie wywołuje nasz partial z nową iteracją. Możemy to naprawić, używając funkcji kolekcje funkcja:
.
Teraz, Szyny automatycznie ustali, który szablon powinien zostać użyty i zainicjuje go tylko raz.
Przetwarzanie w tle
Każdy proces, który jest bardziej czasochłonny i nie jest kluczowy dla bieżącego przepływu, może być uważany za dobrego kandydata do przetwarzania w tle, np. wysyłanie wiadomości e-mail, gromadzenie statystyk lub dostarczanie okresowych raportów.
Sidekiq jest najczęściej używanym klejnotem do przetwarzania w tle. Używa Redis do przechowywania zadań. Pozwala również kontrolować przepływ procesów w tle, dzielić je na osobne kolejki i zarządzać wykorzystaniem pamięci dla każdego z nich.
Pisz mniej kodu, używaj więcej klejnotów
Szyny opracowała ogromną liczbę klejnotów, które nie tylko ułatwiają życie i przyspieszają proces rozwoju, ale także zwiększają szybkość działania aplikacji. Klejnoty takie jak Devise czy Pundit są zazwyczaj dobrze przetestowane pod względem szybkości i działają szybciej i bezpieczniej niż kod napisany na zamówienie w tym samym celu.
W przypadku jakichkolwiek pytań dotyczących poprawy Wydajność Railsówzasięg Inżynierowie The Codest aby skonsultować swoje wątpliwości.