Skip to Main Content
☕ Ủng hộ cafe

☕ Mời mình một ly cafe

Nếu tài liệu này hữu ích, bạn có thể ủng hộ mình một ly cafe để mình có thêm động lực viết tiếp ❤️

QR Support
Ngân hàng: VC Bank
Chủ tài khoản: DO KHAC LAM
Số tài khoản: 7906077097
Nội dung: Ung ho Dokhala
Cảm ơn bạn đã ủng hộ Dokhala 🙏
💬 Liên hệ

💬 Kết nối với mình

Bạn cần hỏi thêm về Oracle APEX, góp ý nội dung, hoặc muốn trao đổi dự án? Có thể nhắn mình qua các kênh dưới đây.

← Quay lại bài viết

Oracle APEX Tabs có thật sự Lazy Load? Tối ưu Page Load từ 9.7s xuống 3.7s

Kiểm tra cách Oracle APEX render các Tabs Region, chứng minh tab không active vẫn có thể chạy SQL/PLSQL trong initial request và xây dựng cơ chế lazy load bằng Ajax để giảm thời gian page load.

Khi xây dựng dashboard trong Oracle APEX, chúng ta thường dùng Tabs hoặc Region Display Selector để chia một page lớn thành nhiều nhóm nội dung.

Trên giao diện, người dùng chỉ nhìn thấy một tab tại một thời điểm. Vì vậy có một giả định rất dễ mắc phải:

Tab đang bị ẩn thì query hoặc PL/SQL của tab đó chắc chưa chạy.

Tuy nhiên, trong một bài kiểm tra thực tế, tôi phát hiện Oracle APEX vẫn có thể render nội dung của tất cả các tab ngay trong initial page request. Tab không active chỉ bị ẩn ở browser.

Điều này trở thành vấn đề performance nếu các tab chứa query lớn, REST API, PL/SQL Dynamic Content hoặc logic xử lý mất nhiều thời gian.

Bài viết này mô phỏng lại quá trình kiểm tra, cách chứng minh bằng APEX Debug, và cách triển khai lazy load để chỉ query dữ liệu khi người dùng thực sự mở tab.

1. Bài toán

Tạo một page test trong Oracle APEX gồm ba tab:

  • COURSES
  • TEACHER COURSES
  • STUDENT COURSES

Mỗi tab sử dụng một bảng có lượng dữ liệu tương đối lớn.

Tab Table Số record trong môi trường test
COURSES COURSES 194,617
TEACHER COURSES TEACHER_COURSES 4,786,882
STUDENT COURSES STUDENT_COURSES 1,873,300

Tab mặc định khi page load là COURSES.

Câu hỏi cần xác định:

Khi page vừa mở, APEX chỉ chạy code của COURSES hay code của TEACHER COURSES và STUDENT COURSES cũng chạy?

2. Điều người dùng thấy và điều server thực sự làm

Browser
COURSES
ACTIVE
TEACHER COURSES
HIDDEN
STUDENT COURSES
HIDDEN
Initial Page Render Request
Query / Render COURSES
Query / Render TEACHER_COURSES
Query / Render STUDENT_COURSES
Điểm quan trọng: Hidden trên browser không đồng nghĩa với Not Rendered trên server.

3. Kiểm tra bằng DOM

Khi inspect HTML của APEX Tabs Region, tab active có trạng thái tương tự:

<div
    id="SR_tab_courses"
    role="tabpanel"
    aria-hidden="false">
    ...
</div>

Trong khi tab chưa active vẫn tồn tại trong DOM:

<div
    id="SR_tab_teacher_courses"
    role="tabpanel"
    aria-hidden="true"
    style="display: none;">
    ...
</div>

Nếu bên trong panel đã có HTML của report hoặc Dynamic Content, đó là dấu hiệu region đã được server render trước đó.

display:none chỉ làm browser không hiển thị nội dung. Nó không làm SQL hoặc PL/SQL phía server biến mất.

4. Kiểm tra bằng Network

Một phép thử đơn giản khác:

  1. Load page ở tab COURSES.
  2. Mở Chrome DevTools → Network.
  3. Chọn Fetch/XHR.
  4. Clear Network.
  5. Click TEACHER COURSES.

Nếu tab hiển thị đầy đủ dữ liệu ngay nhưng không xuất hiện request lớn dùng để lấy HTML hoặc dữ liệu, khả năng cao nội dung đã được tải sẵn từ initial page render.

Một vài request wwv_flow.ajax nhỏ vẫn có thể xuất hiện do APEX framework, Developer Toolbar hoặc plugin. Không nên chỉ nhìn tên request; cần xem payload, response, size và thời gian xử lý.

5. Tạo bài test có thể đo được bằng APEX Debug

Để chứng minh chính xác region nào chạy, mỗi tab được đổi sang PL/SQL Dynamic Content kiểu PL/SQL Function Body returning a CLOB.

COURSES

DECLARE
    l_count NUMBER;
    l_start NUMBER := DBMS_UTILITY.GET_TIME;
    l_clob  CLOB;
BEGIN
    apex_debug.message(
        'TEST_TAB_START: COURSES'
    );

    SELECT COUNT(*)
      INTO l_count
      FROM courses;

    l_clob :=
          '<h3>COURSES</h3>'
       || '<p>Records: '
       || l_count
       || '</p>';

    apex_debug.message(
        'TEST_TAB_END: COURSES - records=%s - elapsed=%s sec',
        l_count,
        ROUND(
            (DBMS_UTILITY.GET_TIME - l_start) / 100,
            2
        )
    );

    RETURN l_clob;
END;

TEACHER COURSES

DECLARE
    l_count NUMBER;
    l_start NUMBER := DBMS_UTILITY.GET_TIME;
    l_clob  CLOB;
BEGIN
    apex_debug.message(
        'TEST_TAB_START: TEACHER_COURSES'
    );

    SELECT COUNT(*)
      INTO l_count
      FROM teacher_courses;

    l_clob :=
          '<h3>TEACHER COURSES</h3>'
       || '<p>Records: '
       || l_count
       || '</p>';

    apex_debug.message(
        'TEST_TAB_END: TEACHER_COURSES - records=%s - elapsed=%s sec',
        l_count,
        ROUND(
            (DBMS_UTILITY.GET_TIME - l_start) / 100,
            2
        )
    );

    RETURN l_clob;
END;

STUDENT COURSES

DECLARE
    l_count NUMBER;
    l_start NUMBER := DBMS_UTILITY.GET_TIME;
    l_clob  CLOB;
BEGIN
    apex_debug.message(
        'TEST_TAB_START: STUDENT_COURSES'
    );

    SELECT COUNT(*)
      INTO l_count
      FROM student_courses;

    l_clob :=
          '<h3>STUDENT COURSES</h3>'
       || '<p>Records: '
       || l_count
       || '</p>';

    apex_debug.message(
        'TEST_TAB_END: STUDENT_COURSES - records=%s - elapsed=%s sec',
        l_count,
        ROUND(
            (DBMS_UTILITY.GET_TIME - l_start) / 100,
            2
        )
    );

    RETURN l_clob;
END;

6. Bật App Trace và kiểm tra initial request

Bật:

Developer Toolbar
→ Debug
→ App Trace

Sau đó refresh page và chưa click TEACHER COURSES hoặc STUDENT COURSES.

Trong View Debug, mở request có:

Path Info = show

Search:

TEST_TAB

Kết quả cho thấy cả ba region đều execute trong cùng initial request.

APEX Debug - Initial Page Request
Path Info: show
COURSES
Records: 194,617
EXECUTED ✓
TEACHER_COURSES
Records: 4,786,882
EXECUTED ✓
STUDENT_COURSES
Records: 1,873,300
EXECUTED ✓
Kết luận: dù browser chỉ đang hiển thị COURSES, PL/SQL của cả ba tab đã chạy.

7. COUNT(*) vẫn quá nhanh để thấy rõ ảnh hưởng performance

Trong môi trường test, các câu COUNT(*) vẫn chạy khá nhanh.

Để tạo một controlled test dễ nhìn, thêm:

DBMS_SESSION.SLEEP(3);

vào mỗi tab ngay sau message START.

Ví dụ:

apex_debug.message(
    'TEST_TAB_START: COURSES'
);

DBMS_SESSION.SLEEP(3);

SELECT COUNT(*)
INTO l_count
FROM courses;
Lưu ý: DBMS_SESSION.SLEEP chỉ được dùng trong bài test DEV/UAT để mô phỏng workload. Không để delay giả này trong production.

8. Kết quả trước khi Lazy Load

Sau khi mỗi tab delay khoảng 3 giây:

Initial Page Load - Before Lazy Load
COURSES
3.01s
TEACHER
3.15s
STUDENT
3.03s
0s 3s 6s 9s
User chỉ đang xem COURSES nhưng phải chờ: ≈ 9.7 seconds

APEX Debug ghi nhận request page có elapsed time khoảng:

9.7275 seconds

Trong khi Maximum Execution Time của một individual operation chỉ khoảng 3 giây.

Điều này cho thấy ba region đang được xử lý nối tiếp trong initial request.

9. Mục tiêu của Lazy Load

Thay vì:

PAGE LOAD
    ↓
COURSES            ~3s
    ↓
TEACHER COURSES    ~3s
    ↓
STUDENT COURSES    ~3s
    ↓
PAGE READY         ~9.7s

Ta muốn:

PAGE LOAD
    ↓
COURSES            ~3s
    ↓
PAGE READY

Click Teacher
    ↓
Load Teacher       ~3s

Click Student
    ↓
Load Student       ~3s

Chi phí xử lý không biến mất. Nó được dời đến thời điểm người dùng thực sự cần dữ liệu.

10. Tạo flag điều khiển từng tab

Tạo hai page item:

P1024_LOAD_TEACHER
P1024_LOAD_STUDENT

Giá trị mặc định:

N

Khi người dùng mở Teacher lần đầu:

P1024_LOAD_TEACHER = Y

Khi mở Student:

P1024_LOAD_STUDENT = Y

11. Sửa PL/SQL của Teacher Courses

Region Teacher ban đầu chỉ return placeholder.

DECLARE
    l_count NUMBER;
    l_start NUMBER := DBMS_UTILITY.GET_TIME;
    l_clob  CLOB;
BEGIN

    IF NVL(:P1024_LOAD_TEACHER, 'N') <> 'Y'
    THEN
        RETURN
            '<div class="lazy-placeholder">'
            || 'Click tab to load Teacher Courses...'
            || '</div>';
    END IF;


    apex_debug.message(
        'TEST_TAB_START: TEACHER_COURSES'
    );


    /* Chỉ dùng để test performance */
    DBMS_SESSION.SLEEP(3);


    SELECT COUNT(*)
      INTO l_count
      FROM teacher_courses;


    l_clob :=
          '<h3>TEACHER COURSES</h3>'
       || '<p>Records: '
       || l_count
       || '</p>';


    apex_debug.message(
        'TEST_TAB_END: TEACHER_COURSES - records=%s - elapsed=%s sec',
        l_count,
        ROUND(
            (DBMS_UTILITY.GET_TIME - l_start) / 100,
            2
        )
    );


    RETURN l_clob;
END;

Trong property Page Items to Submit của region Teacher:

P1024_LOAD_TEACHER

Static ID của region:

tab_teacher_courses

12. Sửa PL/SQL của Student Courses

DECLARE
    l_count NUMBER;
    l_start NUMBER := DBMS_UTILITY.GET_TIME;
    l_clob  CLOB;
BEGIN

    IF NVL(:P1024_LOAD_STUDENT, 'N') <> 'Y'
    THEN
        RETURN
            '<div class="lazy-placeholder">'
            || 'Click tab to load Student Courses...'
            || '</div>';
    END IF;


    apex_debug.message(
        'TEST_TAB_START: STUDENT_COURSES'
    );


    /* Chỉ dùng để test performance */
    DBMS_SESSION.SLEEP(3);


    SELECT COUNT(*)
      INTO l_count
      FROM student_courses;


    l_clob :=
          '<h3>STUDENT COURSES</h3>'
       || '<p>Records: '
       || l_count
       || '</p>';


    apex_debug.message(
        'TEST_TAB_END: STUDENT_COURSES - records=%s - elapsed=%s sec',
        l_count,
        ROUND(
            (DBMS_UTILITY.GET_TIME - l_start) / 100,
            2
        )
    );


    RETURN l_clob;
END;

Page Items to Submit:

P1024_LOAD_STUDENT

Static ID:

tab_student_courses

13. Tại sao không dùng Server-side Condition để loại bỏ region?

Một ý tưởng tự nhiên là đặt condition để Teacher và Student hoàn toàn không render lúc page load.

Nhưng nếu region không tồn tại trong DOM, sau đó JavaScript sẽ không có region instance để gọi:

apex.region(
    "tab_teacher_courses"
).refresh();

Vì vậy cách được dùng trong bài test là:

  • Region vẫn tồn tại.
  • Initial render chỉ return một placeholder rất nhẹ.
  • Khi user mở tab, page item đổi thành Y.
  • Ajax refresh region.
  • Lúc đó mới chạy query thật.

14. Bắt sự kiện chuyển tab

Trong quá trình test, việc bind trực tiếp click vào tab header không hoạt động ổn định với cấu trúc Tabs Region hiện tại.

Ví dụ selector vẫn tìm được element:

$("#SR_tab_teacher_courses_tab").length

// 1

Nhưng event click trực tiếp không phải tín hiệu đáng tin cậy trong case này.

Quan sát DOM cho thấy APEX luôn thay đổi:

aria-hidden="true"

        ↓

aria-hidden="false"

khi một panel trở thành tab active.

Vì vậy có thể quan sát thuộc tính này bằng MutationObserver.

15. Đặt Static ID cho Tabs Region cha

Region cha:

Static ID = test_tabs

Các ID dùng trong solution:

Component ID
Tabs container test_tabs
Teacher panel do APEX tạo SR_tab_teacher_courses
Student panel do APEX tạo SR_tab_student_courses
Teacher region Static ID tab_teacher_courses
Student region Static ID tab_student_courses

16. JavaScript Lazy Load hoàn chỉnh

Đặt đoạn code sau tại:

Page
→ JavaScript
→ Execute when Page Loads
(function () {

    const tabsContainer =
        document.getElementById(
            "test_tabs"
        );


    if (!tabsContainer) {
        return;
    }


    function lazyLoadTab(
        panelId,
        itemName,
        regionId
    ) {

        const panel =
            document.getElementById(
                panelId
            );


        if (!panel) {
            return;
        }


        const isActive =
            panel.getAttribute(
                "aria-hidden"
            ) === "false";


        if (
            isActive
            &&
            $v(itemName) !== "Y"
        ) {

            $s(
                itemName,
                "Y"
            );


            apex.region(
                regionId
            ).refresh();
        }
    }


    function checkTabs() {

        lazyLoadTab(
            "SR_tab_teacher_courses",
            "P1024_LOAD_TEACHER",
            "tab_teacher_courses"
        );


        lazyLoadTab(
            "SR_tab_student_courses",
            "P1024_LOAD_STUDENT",
            "tab_student_courses"
        );
    }


    const observer =
        new MutationObserver(
            checkTabs
        );


    observer.observe(
        tabsContainer,
        {
            subtree: true,
            attributes: true,
            attributeFilter: [
                "aria-hidden"
            ]
        }
    );


    /*
     * Trường hợp page load mà một
     * lazy tab đã active sẵn.
     */
    checkTabs();

})();

17. Flow sau khi áp dụng Lazy Load

PAGE LOAD

Chỉ COURSES chạy query thật

COURSES

Query + Render ≈ 3s

PAGE READY ≈ 3.7s

TEACHER COURSES

User mở tab lần đầu

aria-hidden = false
MutationObserver
P1024_LOAD_TEACHER = Y
apex.region(...).refresh()
Query TEACHER_COURSES

STUDENT COURSES

User mở tab lần đầu

aria-hidden = false
MutationObserver
P1024_LOAD_STUDENT = Y
apex.region(...).refresh()
Query STUDENT_COURSES

18. Tại sao tab chỉ load một lần?

Khi Teacher được load thành công:

P1024_LOAD_TEACHER = Y

Lần sau người dùng chuyển về Teacher, điều kiện:

$v("P1024_LOAD_TEACHER") !== "Y"

không còn đúng.

Do đó apex.region(...).refresh() không chạy lại.

Student sử dụng cơ chế tương tự.

Tab nặng chỉ query một lần khi được mở lần đầu, sau đó việc chuyển qua lại chỉ là thao tác UI.

19. Kết quả Before / After

BEFORE
COURSES 3.01s
TEACHER 3.15s
STUDENT 3.03s
≈ 9.7s
AFTER
COURSES ~3s
TEACHER Lazy
STUDENT Lazy
≈ 3.7s
Initial page load trong controlled test giảm khoảng 6 seconds từ khoảng 9.7s xuống khoảng 3.7s.
Đây không phải benchmark chung cho Oracle APEX. Con số trên đến từ controlled test có DBMS_SESSION.SLEEP(3) để làm rõ sự khác biệt giữa eager rendering và lazy loading. Mức cải thiện thực tế phụ thuộc query, API, số region và workload của ứng dụng.

20. Khi nào Lazy Load đặc biệt hữu ích?

Kỹ thuật này có giá trị lớn khi tab chứa:

  • SQL query lớn.
  • Interactive Report hoặc Classic Report phức tạp.
  • PL/SQL Dynamic Content.
  • REST API hoặc Web Service.
  • Procedure gọi hệ thống bên ngoài.
  • Nhiều vòng lặp để tạo HTML.
  • Chart hoặc report mà user không phải lúc nào cũng xem.
  • Nhiều tab độc lập trên cùng dashboard.

Nếu một tab chỉ mất vài millisecond, lazy load có thể không tạo khác biệt đáng kể.

Nhưng nếu một dashboard có 5–10 tab và mỗi tab tốn vài trăm millisecond hoặc vài giây, initial page load có thể tăng nhanh.

21. Production checklist

Không để DBMS_SESSION.SLEEP trong production.
Đặt Static ID rõ ràng cho region cha và region cần refresh.
Khai báo Page Items to Submit cho các flag lazy load.
Kiểm tra initial page bằng APEX Debug / App Trace.
Kiểm tra Network để chắc chắn tab đang load bằng Ajax khi cần.
Không refresh lại region nếu dữ liệu đã được load và không cần cập nhật.
Nếu dữ liệu phải luôn mới, có thể thay flag Y/N bằng chiến lược refresh phù hợp hơn.
Đo performance trước và sau thay vì chỉ dựa vào cảm giác trên UI.

22. Một số lưu ý khi áp dụng vào page thật

Bài test sử dụng page item:

P1024_LOAD_TEACHER
P1024_LOAD_STUDENT

Khi áp dụng vào page production, cần thay bằng item tương ứng của page thật.

Tương tự:

test_tabs
tab_teacher_courses
tab_student_courses
SR_tab_teacher_courses
SR_tab_student_courses

phải được map lại với Static ID và DOM ID thật của ứng dụng.

Không nên copy nguyên ID của page test sang production mà không kiểm tra DOM.

23. Vì sao cần đo bằng APEX Debug thay vì chỉ nhìn browser?

Browser giúp trả lời:

  • HTML đã tồn tại chưa?
  • Tab có phát sinh Ajax request mới không?
  • Region nào đang hidden?

Nhưng APEX Debug mới giúp trả lời:

  • PL/SQL region có thực sự execute không?
  • Query hoặc process nào đang chiếm thời gian?
  • Elapsed time của toàn request là bao nhiêu?
  • Một operation cụ thể mất bao lâu?

Trong case này, chính App Trace đã chứng minh:

TEST_TAB_START: COURSES
TEST_TAB_END: COURSES

TEST_TAB_START: TEACHER_COURSES
TEST_TAB_END: TEACHER_COURSES

TEST_TAB_START: STUDENT_COURSES
TEST_TAB_END: STUDENT_COURSES

đều nằm trong cùng initial page request.

24. Bài học quan trọng

Đừng giả định một component không nhìn thấy trên UI thì server chưa xử lý component đó.

Khi debug performance Oracle APEX, nên kiểm tra theo nhiều lớp:

  1. DOM.
  2. Network.
  3. APEX Debug / App Trace.
  4. SQL / PL/SQL execution time.
  5. Controlled test để cô lập nguyên nhân.
  6. Before / After measurement.

Cách tiếp cận này giúp tránh tối ưu theo cảm giác và tập trung vào phần thực sự tạo latency.

25. Kết luận

Tabs trong Oracle APEX giúp tổ chức UI rất tốt, nhưng tab đang hidden không mặc nhiên có nghĩa region đó được lazy load.

Trong bài test này:

Before

COURSES          ~3s
TEACHER          ~3s
STUDENT          ~3s
---------------------
Initial Load     ~9.7s

Sau khi chuyển hai tab không mặc định sang cơ chế placeholder + Ajax refresh:

After

Initial Page
    → COURSES only
    → ~3.7s

Open Teacher first time
    → Ajax load Teacher

Open Student first time
    → Ajax load Student

Open loaded tab again
    → No additional query

Đây là một kỹ thuật hữu ích cho những Oracle APEX dashboard có nhiều tab, nhiều report hoặc nhiều source dữ liệu nặng.

Quan trọng nhất không phải là MutationObserver hay một đoạn JavaScript cụ thể, mà là tư duy:

Chỉ thực hiện công việc nặng khi người dùng thực sự cần nó, và luôn đo lại performance sau khi thay đổi.